C# Socket网络编程学习(4-->6)
C# Socket网络编程学习(4-->6)
2010年05月03日
C# Socket网络编程学习(4) - TCP消息边界处理
在前面的几篇中,讲了关于套接字Socket以及利用套接字助手类来进行服务端和客户端之间的通信,在此中间并没有对发送的信息进行任何的处理。在本篇中将会讲一下TCP通信时的信息边界问题。
通 过套接字或其助手类来接收信息时,是从缓存区里一次性把全部的缘存都读取出来,只要你设置的缓存够大,它就能读取这么多,这样就会导致这样的情况出现。如 果服务端连续发送信息到客户端,如我连续发送字符串“message 1”、“message 2”、“message 3”、“message 4”、“message 5”,我预想的是在客户端也是能够收到这样的五个完整的字符串,如果用前二篇中讲的方法,在同台机子上测试的话,是正常的,因为同台机子上网络信息传送出 现的异常会比较少,但如果把客户端与服务端部署在不同的机器上,则会出现一些异想不到的现象。你会发现接收到的字符都被打乱了,会出现如 “3message 4”的字符串,这样的话,我们就不能把服务端发送的信息正常的还原。这个就是消息的边界问题,要解决这个问题,方法有很多,现抽取其中几个来讲一下:
1、固定尺寸的消息
这 是最简单但也是最昂贵的解决TCP消息问题的方案。就是要设计一种协议,永远以固定的长度传递消息,通过将所有的消息都设置为固定的尺寸,在从远程设备中 接收到完整的消息时,TCP接收程序就能够了解发送的情况了。用这各地意味着必须将短消息加长,造成网络带宽资源的浪费。
2、使用消息尺寸信息
这个方案允许使用可变长度的消息,惟一的不足就是接收端的远程设置必须了解每一个变长消息的确切长度。具体的方法是,在发送消息的时候,一起发送该消息的长度。那么在客户端接收的时候就能知道该消息的长度是多少,再来读取消息。
3、使用消息标记
该 方案使用预先确定的一个字符(或多个字符)来指定消息的结束,通过这种方式来分隔不同的消息。但用这种方法必须对所接收到的每一个字符进行检查以便确定为 结束标记,这对于大型消息来说,可能导致系统性能的下降,不过对于C#语言来说,提供了一些类,能够用于简化这个过程,那就是System.IO命名空间 流类,下面我们也着重来讲一下这各方法。至于第二种方法,将在下一篇中与在消息中传送实体类信息相结合来讲述。
在上一篇中,我们已经提到NetworkStream类,利用该类来传送和接收消息。在这里,再提一下另外的二个流类,那就是StreamReader和StreamWriter,这二个类也可用于TCP协议发送和接收文本消息。
当我们得到Socket连接的一个NetworkStream对象时,可以通过下面的方法得到StreamWriter和StreamReader对象。
1NetworkStream ns = s.GetStream();
2 StreamReader sr = new StreamReader(ns);
3 StreamWriter sw = new StreamWriter(ns);
这样我们就可以通过StreamWriter来发送消息,通过StreamReader来接收消息:
1//发送消息
2string welcome = "Welcome to my test sever ";
3
4 sw.WriteLine(welcome);
5 sw.Flush();
接收消息:
1//接收消息
2string data = "";
3data = sr.ReadLine();
这样是不是比以前的做法更简单了,而且同时也解决了TCP消息边界问题了。
但是用这各方法必须得注意以下二点:
1、 这种方法其实就是利用消息标记来解决边界问题的,这里的标记就是换行符,也就是说,StreamWriter中的WriteLine()和 StreamReader中的ReadLine()一定要成对使用,不然如果发送的信息中没有换行符,则客户机中用ReadLine()读取信息时,将无 法结束,将堵塞程序的执行,一直等待换行符。
2、另外还要保证在发送的消息本身不应该带有换行符,如果消息本身带有换行符,则这些换行符将被ReadLine()方法错误地作为标记,影响数据的完整性。
关于TCP消息边界处理就暂时讲到这里了,由于自己的理解也不够深,难免会出现错误,请各位及时纠正。在下一篇中,将讲述传送实体类方面的问题。
C# Socket网络编程学习(5) - 发送和接收实体类数据
在前面讲述的篇幅中,发送的都是文本信息,我们只要通过Encoding中的几个方法把文本转化成二进制数组就可以利用Socket来传输了,这对于一些 基本的信息传输能够得到满足,但对于一些复杂的消息交流,则有些“吃力”。我们有时候会把一些信息封闭在一个类中,如果Socket能够传送类对象,那么 一些复杂的问题能够通过面向对象来解决了,即方便又安全。大家都知道,要想在网络上传输信息,必须要经过序列化才行,所以在传送类对象时,首选必须对该类 对象进行序列化,才能够在网络上进行传输。
序列化类对象有三种序列化方法:
1、Xml序列化
2、Binary序列化
3、Soap序列化
这几种序列化方法,运用方法相类似,只不过用到的类不一样。在这里也不一一讲述了,有兴趣的朋友可以到网上搜一搜,相信会有不少的收获。这里主要讲一下利用Soap序列化来传送消息。
1、首先我们先来建立一个实体类,用来做消息的载体类对象
1using System;
2using System.Collections.Generic;
3using System.Text;
4
5namespace sbwConsole
6{
7 [Serializable]
8 public class SocketData
9 {
10 private OperateType _operateType;
11 private OperateInfo _operateInfo;
12 private string _connString;
13 private string _clientIP;
14 private string _serverIP;
15
16 /**////
17 /// 指令传输数据
18 ///
19 /// 指令类型
20 /// 指令信息
21 /// ASP数据库连接字符串
22 /// 子服务器IP
23 /// ASP服务器IP
24 public SocketData(OperateType operateType, OperateInfo operateInfo,
25 string connString, string clientIP, string serverIP)
26 {
27 _operateType = operateType;
28 _operateInfo = operateInfo;
29 _connString = connString;
30 _clientIP = clientIP;
31 _serverIP = serverIP;
32 }
33
34 /**////
35 /// 指令类型
36 ///
37 public OperateType OperateType
38 {
39 get { return _operateType; }
40 set { _operateType = value; }
41 }
42 /**////
43 /// 指令信息
44 ///
45 public OperateInfo OperateInfo
46 {
47 get { return _operateInfo; }
48 set { _operateInfo = value; }
49 }
50 /**////
51 /// ASP数据库连接字符串
52 ///
53 public string ConnString
54 {
55 get { return _connString; }
56 set { _connString = value; }
57 }
58 /**////
59 /// 子服务器IP
60 ///
61 public string ClientIP
62 {
63 get { return _clientIP; }
64 set { _clientIP = value; }
65 }
66 /**////
67 /// ASP服务器IP
68 ///
69 public string ServerIP
70 {
71 get { return _serverIP; }
72 set { _serverIP = value; }
73 }
74 }
75
76 /**////
77 /// 指令类型
78 ///
79 public enum OperateType
80 {
81 /**////
82 /// 网站操作
83 ///
84 Web = 0,
85 /**////
86 /// 升级
87 ///
88 Upgrade,
89 /**////
90 /// 迁移
91 ///
92 Transfer
93 }
94
95 /**////
96 /// 指令信息
97 ///
98 public enum OperateInfo
99 {
100 /**////
101 /// 发送
102 ///
103 Send = 0,
104 /**////
105 /// 出错
106 ///
107 Error,
108 /**////
109 /// 成功
110 ///
111 Success,
112 /**////
113 /// 重发
114 ///
115 SendAgain
116 }
117}
118
2、发送前先把类对象进行Soap序列化消息发送方法
1public static void Send(NetworkStream ns, SocketData sd)
2 {
3 IFormatter formatter = new SoapFormatter();
4 MemoryStream mem = new MemoryStream();
5
6 formatter.Serialize(mem, sd);
7 byte[] data = mem.GetBuffer();
8 int memsize = (int)mem.Length;
9 byte[] size = BitConverter.GetBytes(memsize);
10 ns.Write(size, 0, 4);
11 ns.Write(data, 0, memsize);
12 ns.Flush();
13 mem.Close();
14 }
这里利用
IFormatter formatter = new SoapFormatter();
MemoryStream mem = new MemoryStream();
formatter.Serialize(mem, sd);
对类对象sd进行序列化。在这里还有一个细节值得一提,那就是消息边界问题的处理,这里是利用发送消息的长度方法来实现。代码如下:
1int memsize = (int)mem.Length;
2 byte[] size = BitConverter.GetBytes(memsize);
3 ns.Write(size, 0, 4);
通过BitConverter.GetBytes()方法可以把数据类型转化为二进制数组,从而可以在网络上传送,所以在接收的时候先接收消息长度,再通过该长度来循环读取完整的消息。
3、接收消息接收消息方法
1public static SocketData Receive(NetworkStream ns)
2 {
3 MemoryStream mem = new MemoryStream();
4 SocketData sd;
5 byte[] data = new byte[4];
6 int revc = ns.Read(data, 0, 4);
7 int size = BitConverter.ToInt32(data, 0);
8 int offset = 0;
9
10 if (size > 0)
11 {
12 while (size > 0)
13 {
14 data = new byte[1024];
15 revc = ns.Read(data, offset, size);
16 mem.Write(data, offset, revc);
17 offset += revc;
18 size -= revc;
19 }
20
21 IFormatter formatter = new SoapFormatter();
22 mem.Position = 0;
23 sd = (SocketData)formatter.Deserialize(mem);
24 mem.Close();
25 }
26 else
27 {
28 sd = null;
29 }
30 return sd;
31 }
通过sd = (SocketData)formatter.Deserialize(mem);还原数据为类对象,就可以对此类对象进行访问了。用Xml序列化或用二 进制序列化也是类似,只不过把序列化的方法改一下就可以了,一般来说用二进制序列化得到的数据最小,占用带宽也最小,而用xml和Soap来序列化,都是 序列化为Xml格式,所以数据比较大,占用带宽比较大。但用xml或Soap序列化,兼容性高,可以兼容不同系统之间的通信,而二进制不行。可以说各有利 弊,可以根据实际情况来选择哪一种序列化。
C# Socket网络编程学习(6) - 使用线程池提高性能
在前几篇介绍中,不论是服务端的侦听还是客户端的连接都是通过新建一个线程去执行特定功能的。在这种情况下,一量有一个新客户端需要连接,则又得创建新的 线程,而当程序创建新线程时,往往需要大量的内部开销,这对程序的性能有一定的影响。在.NET库中提供了一种方法,可以避免一些开销。而在Socket 通讯中还有另一种访求那就是异步Socket,我不知道用这种方式的性能如何,在这里且不管这种形式,主要来看一下用线程池解决问题。
Windows操作系允许用户维持一池“预先建立的”线程,这个线程池为应用程序中指定的方法提供工作线索。一个线程控制线程池的操作,并用应用程序可以分配附加的线程进行处理。在默认情况下,在线程池中有25个预处理线程,用这种方式可以满足一些小应用。
如果要为线程池中的线程注册一个代表,则用下面的格式:
ThreadPool.QueueUserWorkItem(new WaitCallback(Counter));
其 中QueueUserWorkItem是ThreadPool类的一个静态方法;而Counter参数代表运行在线程中的方法,在这要注意的是该 Counter方法必须包含一个object 参数,这个在下面的例子中有体现;另外,处Thread对象不一样,代表一旦放置在线程池查询中上,将被处理,不需要其他的方法启动该项工作;当主程序线 程退出,所有的线程池线程都将终止,主线程不会等待线程池线程结束。
下面我们来看看怎么样运用到我上面讲的例子中去:
原有线程调用:
clientThread = new Thread(new ThreadStart(ReceiveData));
clientThread.Start();
用线程池只要一句就可以了:
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData));