当前位置: 代码迷 >> 编程 >> 施用TCP/ IP套接字
  详细解决方案

施用TCP/ IP套接字

热度:5333   发布时间:2013-02-26 00:00:00.0
使用TCP/ IP套接字

使用TCP/ IP套接字

 

TCP/IP 套接字提供了网络上的低级控制。TCP/IP 套接字是两台计算机之间的逻辑连接,有了它,计算机在任何时候可以发送或接收数据。在计算机显式发出关闭指令之前,这个连接一直保持。它提供了高度的灵活性,但也带来了大量的问题,在这一章我们会看到。因此,除非真的需要非常高度的控制,最好不要使用更抽象的网络协议,在这一章的后面我们也会谈到。

为了使用 TCP/IP 套接字,必须用到包含在命名空间 System.Net 中的类,汇总在表 10-1 中。

表10-1. 使用 TCP/IP 套接字需要的类

描述

System.Net.Sockets.TcpListener

服务器用这个类监听入站请求。

System.Net.Sockets.TcpClient

服务器和客户端都使用这个类,控制如何在网络上发送数据。

System.Net.Sockets.NetworkStream

这个类用于在网络上发送和接收数据。它在网络上发送字节,因此,要发送文本,通常要打包到其他的流类型中。

System.IO.StreamReader

这个类为了读取文本,用于打包 NetworkStream 类。StreamReader 提供两个方法 ReadLine 和 ReadToEnd,以字符串形式返回流数据。在 StreamWriter 创建时,各种不同的文本编码可以使用,由System.Text.Encoding 类的实例提供。

System.IO.StreamWriter

这个类用于打包 NetworkStream 类,为了写文本。StreamWriter 提供了两个方法 Write 和 WriteLine,以字符串形式写数据流。

在 StreamWriter 创建时,各种不同的文本编码可以使用,由System.Text.Encoding 类的实例提供。

这一章的第一个例子,我们创建一个聊天程序,包含一个聊天服务器(清单 10-1 )和一个客户端(清单10-2 )。服务器的任务是等待并监听客户端,客户端连接以后,它要求客户提供用户名;它还必须连续监听所有客户端的入站消息,接收了一个入站消息以后,就把这个消息推给所有的客户端。客户端的任务是连接到服务器,并提供一个界面,使用户能够读取接收到的消息,写消息并发送给其他用户。TCP/IP 连接非常适合这种类型的应用程序,因为,连接一直保持,使服务器直接把入站消息推出去,而不必从客户端拉。

 

清单10-1. 聊天服务器

#light

open System

open System.IO

open System.Net

openSystem.Net.Sockets

open System.Threading

openSystem.Collections.Generic

 

type ClientTable() = class

    let clients = newDictionary<string,StreamWriter>()

    /// Add a client and its stream writer

    member t.Add(name,sw:StreamWriter) =

        lockclients (fun () ->

            if clients.ContainsKey(name)then

               sw.WriteLine("ERROR - Name in usealready!")

               sw.Close()

            else

               clients.Add(name,sw))

    /// Remove a client and close it, if no one else has donethat first

    member t.Remove(name) =

        lockclients (fun () ->clients.Remove(name) |> ignore)

    /// Grab a copy of the current list of clients

    member t.Current =

        lockclients (fun () ->clients.Values |> Seq.toArray)

    /// Check whether a client exists

    member t.ClientExists(name):bool =

        lockclients (fun () ->clients.ContainsKey(name) )

end

type Server() = class

    let clients = newClientTable()

    let sendMessage name message =

        let combinedMessage =

           Printf.sprintf "%s: %s" namemessage

        for sw inclients.Current do

            try

               lock sw (fun () ->

                   sw.WriteLine(combinedMessage)

                   sw.Flush())

            with

            | _-> () // Someclients may fail

    let emptyString s = (s = null|| s = "")

    let handleClient (connection : TcpClient) =

        let stream = connection.GetStream()

        let sr = newStreamReader(stream)

        let sw = newStreamWriter(stream)

        let recrequestAndReadName() =

           sw.WriteLine("What is your name? ");

            sw.Flush()

            let rec readName() =

               let name = sr.ReadLine()

               if emptyString(name)then

                   readName()

               else

                   name

            let name = readName()

            if  clients.ClientExists(name)then

               sw.WriteLine("ERROR - Name in usealready!")

               sw.Flush()

               requestAndReadName()

            else

               name

        let name = requestAndReadName()

       clients.Add(name,sw)

        let rec listen() =

            let text = trySome(sr.ReadLine()) with _ -> None

            match text with

            |Some text ->

               if not (emptyString(text))then

                   sendMessage name text

                Thread.Sleep(1)

               listen()

            |None ->

               clients.Remove name

               sw.Close()

       listen()

    let server = newTcpListener(IPAddress.Loopback, 4242)

    let rechandleConnections() =

        server.Start()

        if (server.Pending()) then

            let connection = server.AcceptTcpClient()

           printf "New Connection"

            let t = new Thread(fun () ->handleClient connection)

           t.Start()

       Thread.Sleep(1);

        handleConnections()

    member server.Start() = handleConnections()

end

(newServer()).Start()

[

Seq.to_array 已经改成 Seq.toarray

 

    member t.ClientExists(name) =

        lockclients (fun () ->clients.ContainsKey(name) |> ignore)

改成:

    member t.ClientExists(name):bool =

        lockclients (fun () ->clients.ContainsKey(name) )

 

    let rechandleConnections() =

改成:

    let rec handleConnections() : unit =

 

上述两个修改并非必需。

把代码保存为脚本文件,比如:p10-01.fsx

编译:fsc p10-01.fsx

]

 

我们从头开始看一下清单 10-1 的程序。第一步定义一个类,管理连接到服务器的客户端。成员 Add、Remove、Current 和 ClientExists 共享不可变的字典,由这个绑定定义:

 

let clients = newDictionary<string,StreamWriter>()

 

它包含了客户端名字与连接的映射,这对程序中的其他函数是隐藏的。Current 成员把映射中的实体复制到一个数组中,保证在枚举时不会有改变列表的危险,这会引起错误。仍可以用 Add 和 Remove 修改客户端的集合,在下一次调用 Current 时更新可用。因为代码是多线程的,Add 和 Remove 的实现要锁定客户端集合,以保证多线程同时更新集合时,集合的改变不会丢失。

下一个定义的函数是 sendMessage,用 Current 成员获得客户端的映射,用列表解析式进行枚举,把消息发送给集合中的每个客户端。注意一下,在写入之前是如何锁定 StreamWriter 类的:

 

lock sw (fun () ->

sw.WriteLine(message)

sw.Flush())

 

这是为阻止多个线程同时写,它会引起文本在客户端的屏幕上出现混乱。再定义 emptyString 函数,这是一个非常有用的小函数,把重复使用的一些动作进行打包。再定义 handleClient 函数,管理客户端的新连接,它分解成几个函数。handleClient 函数它由最后定义的函数 handleConnections 调用。在专门分配的新线程上调用,去管理打开的连接。handleClient 要做的第一件事就是获得的流,表示网络连接和打包在 StreamReader 和 StreamWriter 中:

 

let stream = connection.GetStream()

let sr = new StreamReader(stream)

let sw = new StreamWriter(stream)

 

有办法把读写流分开是有用的,因为这个函数本身就是完全分开的。我们已经看过 sendMessage 函数,用于给客户端发送消息,后面还会看到一个新的线程,专门分配给读客户端。

在 handleClient 中定义内部函数 requestAndReadName,是很简单的,只重复询问用户名,直到名字不空或 null 字符串,并且没有在用。有了用户名以后,再用 addClient 函数把它添加到客户端集合中:

 

let name = requestAndReadName()

addClient name sw

 

handleConnection 的最后一部分是定义 listen 函数,它负责监听客户端的入站消息。这里,从流中读取一些文本,打包在 try 表达式中,用选项类型的 Some/None 值来表示读取的不管是什么文本:

 

let text = try Some(sr.ReadLine()) with _-> None

 

    然后,使用模式匹配决定下一步做什么。如何成功读取文本,就用 sendMessage 函数发送消息给所有的客户端;否则,就从客户端集合中删除自己,并退出函数,接着,线程管理的连接也退出。

 

注意: 虽然 listen 函数是递归的,可能会被多次调用,但是没有堆栈溢出的危险,是因为函数是尾递归,编译器发出专门的尾指定,告诉.NET 运行时调用这个函数,不使用堆栈保存参数和局部变量。在F# 中定义的任何递归函数,递归调用在函数的最后才发生,是尾部递归。

 

接下来,创建TcpListener 类的实例。这个类是真正实现监听入站连接的,通常用监听服务器的 IP 地址和端口号来初始化。启动监听器时,提供监听 IPAddress 的地址,任意地址,监听器就会监听和这台计算机上网卡所关联的 IP 地址上的所有通信。然而,因为这是一个演示程序,为 TcpListener 类指定监听 IPAddress.Loopback,表示只从本地计算机搜集请求。端口号告诉网络通信只为这个应用程序服务,而不是其他的应用程序。使用 TcpListener 类,使一个监听器一次只监听一个端口是可能的。端口号的选择可以随便一点,但是,应该要大于1023,因为端口号0 到 1023 是留给专门的应用程序。因此,在函数的最后,程序定义了handleConnections,TcpListener 实例在端口 4242 上创建监听器:

 

let server = newTcpListener(IPAddress.Loopback, 4242)

 

    这个函数是个无限循环,监听新的客户端连接,并创建新的线程来管理。看下面的代码,有了连接以后,用它来获得连接的一个实例,并启动一个新线程去管理它。

 

let connection = server.AcceptTcpClient()

print_endline "New Connection"

let t = new Thread(fun () -> handleClientconnection)

t.Start()

 

    现在,我们知道了服务器是如何工作的,下面要看客户端,它在很多方面比服务器简单得多。清单 10-2 是客户端的完整代码,下面有相关的讨论。

 

列表10-2. 聊天客户端

#light

open System

open System.ComponentModel

open System.IO

open System.Net.Sockets

open System.Threading

open System.Windows.Forms

 

let form =

    let temp = new Form()

    temp.Text <- "F#Talk Client"

    temp.Closing.Add(fune ->

        Application.Exit()

        Environment.Exit(0))

    let output=

        newTextBox(Dock = DockStyle.Fill,

            ReadOnly = true,

            Multiline = true)

    temp.Controls.Add(output)

    let input =new TextBox(Dock = DockStyle.Bottom, Multiline=true)

    temp.Controls.Add(input)

    let tc = new TcpClient()

    tc.Connect("localhost",4242)

    let load()=

        letrun() =

            letsr = new StreamReader(tc.GetStream())

            while(true)do

                lettext = sr.ReadLine()

                iftext <> null && text <> "" then

                    temp.Invoke(new MethodInvoker(fun()->

                        output.AppendText(text+ Environment.NewLine)

                        output.SelectionStart<- output.Text.Length))

                    |> ignore

        let t =new Thread(newThreadStart(run))

        t.Start()

    temp.Load.Add(fun_ -> load())

    let sw = new StreamWriter(tc.GetStream())

    let keyUp _=

        if(input.Lines.Length> 1) then

            lettext = input.Text

            if(text <> null && text <> "") then

                begin

                    try

                        sw.WriteLine(text)

                        sw.Flush()

                    witherr ->

                        MessageBox.Show(sprintf"Server error\n\n%O" err)

                        |> ignore

                end;

                input.Text <- ""

    input.KeyUp.Add(fun_ -> keyUp ())

    temp

[<STAThread>]

do Application.Run(form)

 

[

   input.KeyUp.Add(fun _ -> keyUp e)

改成:

   input.KeyUp.Add(fun _ -> keyUp ())

把代码保存为脚本文件,比如:p10-02.fsx

编译:fsc p10-02.fsx

运行服务器端程序,p10-01

运行两个以上的客户端程序,p10-02

]

 

图 10-1 客户端-服务器应用程序运行的结果。


图10-1. 聊天客户端-服务器应用程序

 

现在我们就来看一下清单 10-2 的客户端是如何工作的。客户端的第一部分代码完成窗体各部分的初始化,这不是我们现在感兴趣的,在第八章有更详细的有关 WinForms 应用程序的内容。清单 10-2 的第一部分是有关连接到服务器的 TCP/IP 套接字编程内容。通过创建 TcpClient 类的实例,然后调用 Connect 方法:

 

let tc = new TcpClient()

tc.Connect("localhost", 4242)

 

在这个例子中,我们用 localhost,表示本地计算机,端口 4242,监听这台服务器上的端口。在更实际的例子中,可以用服务器的 DNS 名字,或者由用户指定 DNS 名字。如果运行在同一台计算机上,localhost 也是不错的选择。

从服务器读数据的函数是 load 函数,再把它附加到窗体的 Load 事件,保证窗体装载并初始化后能够运行,需要与窗体的控件交互:

 

temp.Load.Add(fun _ -> load())

 

为了及时从服务器上读出所有数据,需要创建一个新的线程去读所有的入站请求。要做到,定义函数 run,然后,用于启动一个新线程:

 

let t = new Thread(new ThreadStart(run))

t.Start()

 

    在 run 函数的定义中,先创建 StreamReader 从连接中读文本,然后无限地循环,这样,保证了线程不停地从连接中读数据。一旦有了数据之后,必须用窗体的 Invoke 方法更新窗体,这是因为不能从创建窗体之外的线程中更新窗体:

 

temp.Invoke(new MethodInvoker(fun () ->

output.AppendText(text + Environment.NewLine)

output.SelectionStart <- output.Text.Length))

 

    客户端中的另外一个重要功能是把消息写到服务器上。把 keyUp 函数附加到输入文本框的 KeyUp 事件,因此,每次在文本框中按键,代码会触发:

 

input.KeyUp.Add(fun _ -> keyUp e)

[

以上这一句好像不能通过编译,要改成:

input.KeyUp.Add(fun _ -> keyUp () )

]

 

keyUp 函数的实现是很简单,如果文本超过一行,表示已按下回车键,通过网络发送任何可用的文本,并清除文本框。

现在,我们已经了解了客户端和服务器,再看看几个有关这个应用程序的一般问题。在清单 10-1 和 10-2 中,在每次网络操作后都调用了 Flush()。否则,通过网络传输的数据要等到流缓存满才进行,这会导致用户必须输入很多消息,才能出现在另外用户的屏幕上。

这种实现有一些问题,特别是在服务器端。为每个入站的客户端分配一个线程,可以保证对每个客户端有很好的响应,但是,随着客户连接数量的增加,这样,就需要为这些线程大量的上下文切换,导致服务器的整体性能下降。另外,每个客户端都要有它自己的线程,因此,客户端的最大数量就受限于进程所能包含的最大线程数量。这些问题是可以解决的,通常也很容易,只要使用一些更加抽象的协议,下面会有讨论。

  相关解决方案