对于分布式即时通讯程序的思考和实践
计算机网络最根本的目的是实现网络中计算机之间的分布式进程间通信
即时通讯所使用的传输协议是TCP和UDP
windows系统和linux系统都支持socket 按照这样的规范 我们可以实现垮平台的信息交换
所以我们都经过socket传输数据,TCP和UDP位于网络中的传输层,位于IP层之上 是用户功能的最底层
TCP:面向连接的、可靠的、基于字节流的运输层通信协议 数据一定是可靠地到达,先发送的先到,丢包重传 可以提供流控制机制,建立一个TCP连接需要经过3次握手,关闭一个TCP连接需要经过4此握手 ,在一个TCP连接中只支持两方的通信,不支持广播,采用字节流方式,如果字节流太长,将其分段。
UDP:无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。选择UDP必须要谨慎。在网络质量令人不十分满意的环境下,UDP协议数据包丢失会比较严重,但是UDP不是面向连接的 它有资源消耗小,处理速度快的特点 对于那些要求不是特别严格的数据 如音频流,视频流和普通数据 即时偶尔丢几个包也不会对结果产生多大的影响。并且它能减轻对网络的压力
对于即时通讯软件,例如像腾讯QQ 百度Hai 阿里旺旺 以及大型的聊天室,我的理解应该是这样的
有时候它们需要可靠的连接,有时候可能需要UDP就够了 所以像QQ那样的软件是同时使用TCP和UDP这两种协议的
可以设想服务端负责处理客户端的数据 具体包括如响应客户端的好友列表请求,通知其它用户好友的状态更改上线下线,转发文字消息
而客户端直接可以进行视频和语言聊天 视频语音数据量大,服务器也无法承担这样的压力,所以服务端不会对语音视频进行转发 所以应当在两个客户端直接建立连接实现两个客户端进程间的通信 此部分服务器不管。
所以即时通讯软件应该具有这样的特点:
1.服务端处理小的数据
2.大的数据(如语音 视频 文件)在客户端直接进行传送 无须服务器转发
C#局域网聊天的程序源代码在网上到处都是 千篇一律地同一种方式,都是基于TCP协议的
服务端:启动后在内存中维持一个在线客户列表(数据结构为哈希表或者字典),之后开启一个线程使用循环监听客户端的TCP连接 客户端点击登录后请求建立连接,服务端则发送在线列表并通知其它用户有新用户上线了,并将这个TCP连接交给一个新的线程去执行,这个新线程的任务就是循环地接受新连接的数据 并处理这些数据。于是服务端便这样工作了 不停地响应TCP连接,一旦有新用户就创建新的线程专门为该用户服务 这样服务端就可以做到同时响应多个客户的多条请求 很容易地就实现了局域网聊天
但是这里就有一些问题了:运行于Internet上的大型聊天室能这样做吗?
个人觉得肯定不能.如果这个程序是运行于Internet上的 在线用户数以万计 服务端就需要和数万个用户保持这样一个TCP连接 并且服务端上有上万个线程在运行着
而用户大多数时候只是挂着聊天工具 于是发生了这样一种情况,TCP连接数等于在线用户数 线程数等于在线用户数,而这些TCP连接绝大多数时间是没有数据传输的 而这些线程绝大多数时间也是没有执行实际任务的,但是它们依然消耗着网络资源和CPU资源 线程依然在轮流地使用着CPU时间片,它们消耗着资源却什么也没做。还会产生一个矛盾,给服务器带来极大的压力 我相信没有哪个程序能运行上万个线程吧!于是有了以下的突出问题:
1.如何解决线程数 有效地理由CPU
2.如何解决连接数
先说第一个问题,解决线程数 .NET 提供了ThreadPool类,我们通过向线程池指派任务而不用使用new Thread()来为每一个客户端创建一个线程。但是这样又会发生新的问题了 由于线程池中的线程执行任务的时间是从客户登陆到客户下线,所以可以把任务耗时看做的无限的 线程池规定了最大连接数,尽管可以通过设置来更改最大任务数容纳更多的线程 但太多的认为仍然需要排队,这可以有效的控制线程数量 但是依然是治标不治本的,运行的线程依然绝大多数时候只是占着CPU而没有处理用户发送过来的数据 因为用户在线10个小时 服务器处理10个小时信息可能只需要1秒钟 执行这些任务的线程大多数时候只是在等待毫无无用武之地,当用户数太多之后服务端的响应就变得十分迟钝。所以像这种服务端的工作方式就决定了它只能用于在线人数很少局域网聊天 无法进行大型多人的即时通讯。经过一段时间之后 我开始接触到异步编程,于是对此进行了一些更改,以充分地利用CPU
具体方式是这样的:
1.服务器运行我们依然需要一个线程持续地响应客户端的连接请求
以下是监听线程执行的任务
2 {
3 while (running)
4 {
5 remoteClient = listener.AcceptTcpClient();//同步方法
6 RemoteClient newclient = new RemoteClient(remoteClient);
7 }
8
9 }
每当我们接收到一个新的连接后 我们就实例化一个新的RemoteClient对象 而在实例化这个对象中进行异步操作,这里关键就是这个RemoteClient类
2 {
3 private TcpClient client;
4 private NetworkStream streamToClient;
5 private const int bufferSize = 8192;
6 private byte[] buffer;
7 private string userName;
8
9 public RemoteClient(TcpClient client)
10 {
11 this.client = client;
12 streamToClient = client.GetStream();
13 buffer = new byte[bufferSize];
14
15 //异步操作完成时调用的方法
16 AsyncCallback callBack = new AsyncCallback(ReadComplete);
17 streamToClient.BeginRead(buffer, 0, bufferSize, callBack, null); //异步操作完成后将通知回调方法
18 }
19
20 private void ReadComplete(IAsyncResult ar) //参数表示异步操作的状态
21 {
22 int bytesRead = 0;
23 CustomMessage message;
24 try
25 {
26 lock (streamToClient)
27 {
28 bytesRead = streamToClient.EndRead(ar);
29 }
30 if (bytesRead == 0)
31 throw new Exception("读取到0字节");
32 message = (CustomMessage)SerializationHelper.Deserialize(buffer);
33 switch (message.cmd)
34 {
35 case Cmds.online:
36 {
37 //把链接的用户加入到哈希表中
38 Server.userTable.Add(message.message, client);
39 userName = message.message;
40 Server.richTextBox1.AppendText("已响应连接请求" + client.Client.LocalEndPoint.ToString() + "<---" + client.Client.RemoteEndPoint.ToString() + "\n"); ;
41 Server.richTextBox1.AppendText("用户 " + message.message + " 上线了\n");
42
43 CustomMessage tempMessage = new CustomMessage(userName, Cmds.online);//上线信息
44 //通知其它用户
45 foreach (object c in Server.userTable.Values)
46 {
47 NetworkStream tempStream = ((TcpClient)c).GetStream();
48 byte[] tempBuffer = SerializationHelper.Serialize(tempMessage);//序列化
49 tempStream.Write(tempBuffer, 0, tempBuffer.Length);
50 }
51 break;
52 }
53 case Cmds.messageToAll:
54 {
55 Server.richTextBox1.AppendText("接收到数据" + message.message + "\n");
56 break;
57 }
58 default:
59 {
60 break;
61 }
62 }
63
64
65
66 Array.Clear(buffer, 0, buffer.Length);//清空缓存
67
68 streamToClient.Flush();//刷新流中数据 保留此方法供将来使用
69
70 lock (streamToClient)
71 {
72 AsyncCallback callBack = new AsyncCallback(ReadComplete);
73 streamToClient.BeginRead(buffer, 0, bufferSize, callBack, null);
74 }
75 }
76 catch (Exception ex)
77 {
78 Server.userTable.Remove(userName);
79 Server.richTextBox1.AppendText(userName + "下线了!\n");
80 //从哈希表删除该用户
81
82 if (streamToClient == null)
83 streamToClient.Dispose();
84 client.Close();
85 }
86 }
87 }
这里我们看到在实例化对象的时候定义了一个异步回调函数的委托 我们获取streamToClient(TCP连接的数据流)之后开始异步读数据操作,当第一次读操场完成后它将调用回调方法,回调方法处理接收到的数据 并再次进行异步读操作,并在读操作完成后回调自身再次处理数据 再次异步读 再回调自身,这样形成了一个类似在while循环中使用同步的Read()方法的效果 线程数会得到更有效的控制,异步编程本质上虽然是多线程,但是它在某些环境下比直接使用多线程有莫大的好处 这里我们用异步代替了创建新的线程或者是直接使用线程池 。服务端的线程数将得到真正的有效控制,并且由于是异步调用 将能充分地利用CPU提高服务端的性能.当采用这种异步方式后 服务端处理能力的提高将是之前局域网聊天中服务端的成千上万倍。到这里 对于线程数,服务端性能和充分利用CPU算有那么一个小小的成果了 采用这种方式我们可以根据服务能承载用户数设置一些参数 这样在监听线程中可以加以判断,当在线用户已达到服务器极限的时候 我们给新的客户端服务器忙的提示,拒绝其登陆,就像web服务器那样 当连接数超过最大并发连接数的时候就返回一个错误页面
2.对于TCP连接数 TCP既然是面向连接的,只要没有显示的关闭连接 则这个连接将一直存在 由于我刚接触网络编程不久,对于一台服务器是否有TCP连接数的限制也并不是很清楚 并且这些连接对服务器会产生怎样的影响也不清楚,我将继续探索这方面的知识 。目前我有这样的初步想法 只是在用户请求关键数据的时候建立TCP连接 传输完毕后立即关闭,需要时再连接 用完后再关闭。
希望对网络编程以及即时通讯有经验的的朋友能在回复中给我提供一个大致学习的方向或者是某些建议 或者是对即时通讯软件的工作和处理方式能有一点提示,或者通过电子邮箱发过来一点资料