MyException - 我的异常网
当前位置:我的异常网» 编程 » 施用TCP/ IP套接字

施用TCP/ IP套接字

www.MyException.Cn  网友分享于:2015-08-26  浏览:4次
使用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()。否则,通过网络传输的数据要等到流缓存满才进行,这会导致用户必须输入很多消息,才能出现在另外用户的屏幕上。

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

文章评论

程序员都该阅读的书
程序员都该阅读的书
漫画:程序员的工作
漫画:程序员的工作
中美印日四国程序员比较
中美印日四国程序员比较
程序员应该关注的一些事儿
程序员应该关注的一些事儿
程序员和编码员之间的区别
程序员和编码员之间的区别
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
Java程序员必看电影
Java程序员必看电影
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
鲜为人知的编程真相
鲜为人知的编程真相
代码女神横空出世
代码女神横空出世
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
为什么程序员都是夜猫子
为什么程序员都是夜猫子
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
10个调试和排错的小建议
10个调试和排错的小建议
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
程序员的鄙视链
程序员的鄙视链
每天工作4小时的程序员
每天工作4小时的程序员
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
 程序员的样子
程序员的样子
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
我是如何打败拖延症的
我是如何打败拖延症的
旅行,写作,编程
旅行,写作,编程
那些争议最大的编程观点
那些争议最大的编程观点
总结2014中国互联网十大段子
总结2014中国互联网十大段子
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
我的丈夫是个程序员
我的丈夫是个程序员
如何成为一名黑客
如何成为一名黑客
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
程序员必看的十大电影
程序员必看的十大电影
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
老程序员的下场
老程序员的下场
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有