socket客户端的几个疑点

fanbingyuan 2011-02-01 02:02:47
C/S模式下点对多。客户端是怎么接收来自服务器不定时指令的。
我想实现就是像qq弹窗那样的效果,服务器不定时向客户端发送指令,客户端却是不知如何接受。
想了几个思路,求大牛指点指点。
1:客户端socket异步接收,根据接收到的服务器指令完成与服务器的交互。比如客户端a向服务器发送指令x 服务器将指令x发给客户端b。疑点:异步接收如何接受。假如b先向服务器发送指令y,服务器回复指令z,但x比z先到达,会不会产生什么严重的后果。
2:客户端与服务器建立三个连接。一个连接保持与服务器的长连接。一个连接实现与服务器同步通信。一个连接异步接收服务器的指令,实现客户端之间交互。疑点:这样一来服务器负担翻了三番,同时也不知道如何在服务器端区分同一个客户端的三个连接那个是哪个。
3:客户端做成一个简单服务器端,检测端口侦听来自四面八方的连接请求。疑点:客户端的ip不固定。
...全文
973 20 打赏 收藏 转发到动态 举报
写回复
用AI写文章
20 条回复
切换为时间正序
请发表友善的回复…
发表回复
qingtianweichong 2013-03-06
  • 打赏
  • 举报
回复
学习了! 感觉楼上的各位都是大牛!
  • 打赏
  • 举报
回复
上面tcp服务的初创过程的代码可以看出,总的来说,服务器的设计就是“一问一答”的形式的,只不过是异步的。所谓为客户端socket保存起来,只是在ProcessCommand中遇到客户端用户登录的命令时,这时候假设我们编写长连接并且要主动推送消息的服务,才把用户id、客户端socket保存起来。而这属于业务处理层面,并不是说凡是服务器都要保存客户端的socket。

对于端连接方式,要从服务器推送消息给客户端,则可能就是需要在用户登录时得到客户端的ip以及它在登录信息中所说明的监听端口,作为以后推送消息时connect的endpoint。或者(如果客户端是在另一个内网的话)此时需要打洞,并维系另外一个从服务器到客户端的连接。
  • 打赏
  • 举报
回复
我写一部分、初创时期的服务器端的代码做个demo,它刚好可以将明白概念。

假设我们要监听端口port,可以这样开启服务(假设服务器使用一个全局的private static TcpListener server变量引用):

server= new TcpListener(new IPEndPoint(IPAddress.Any, port);
server.Start();
server.BeginAcceptTcpClient(ClientConnected, null);

这样,服务器就注册好了,等着如果有客户端会话连接,就会执行回调方法Connect。

当有一个会话连接,那么就要开始对这个会话进行处理。但是同时会有许多会话出现,所以需要动态地产生会话对象用来记录会话的最基本的信息。会话至少包括以下三个属性:
class Session
{
public TcpClient Client; //客户端连接
public byte[] Buffer; //异步读数据的缓冲区
public MemoryStream Cache; //读取到消息结束标志之前累计读取到的字节
}


那么,处理连接的方法就是这样的:
private void ClientConnected(IAsyncResult handler)
{
var client = this.EndAcceptTcpClient(handler);
var buffer = new byte[40960];
var session = new Session { Client = client, Buffer = buffer, Cache = new MemoryStream() };
client.GetStream().BeginRead(buffer, 0, buffer.Length, Process, session);
this.BeginAcceptTcpClient(ClientConnected, null);
}
当一个客户端会话连接,我们要做两件式,开始异步读这个会话数据,以及继续监听其它会话。其中,当开始异步读的时候,要把当前会话信息作为参数(最后一个参数)传递给处理读到消息的方法Process。这样,读取当前会话消息是异步的,处理其它客户端连接也是异步的。

当会话中读取到客户端有消息传过来,底层的.net系统会在一个系统线程中回调我们的方法Process,我们就在这里解析消息内容,处理返回,然后(根据是短连接还是长连接决定是否关闭这个client连接。看看Process的顶层代码之前,我先做一个假设,假设我们的消息都是在一行之中的utf8文本,使用Json格式。相信使用web编程的人特别熟悉json了。json编码格式比xml快很多而且短小,可以很方便地使用.net系统中的现成的类进行对对象序列化和反序列化。因此我们假设每一个消息,客户端都是把对象序列化为json格式的文本,并且以utf8格式编码,然后传递到服务器,每一个消息都是以回车或者换行符作为结束。这样我们的demo就比较简单,注意是一回车或者换行为消息结束(为了简单,我省略了一个容错语句,如果你进行大规模的压力测试就可能可以遇到这个异常并处理):
private void Process(IAsyncResult handler)
{
var session = (Session)handler.AsyncState;
int len = 0;
len = session.Client.GetStream().EndRead(handler);
if (len > 0)
{
session.Cache.Write(session.Buffer, 0, len);
var c = session.Buffer[len - 1];
if (c == 13 || c == 10)
{
session.Cache.Position = 0;
var rs = new StreamReader(session.Cache, Encoding.UTF8);
while (!rs.EndOfStream)
{
var ln = rs.ReadLine().Trim();
ProcessCommand(ln, session.Client);
}
session.Cache = new MemoryStream(); //如果是短连接,这里直接写 return; 代码
}
}
session.Client.GetStream().BeginRead(session.Buffer, 0, session.Buffer.Length, Process, handler.AsyncState);
}
仅当buffer中读取的最后一个字符恰好是回车或者换行时,我们才循环从整个消息缓冲区中逐一取出消息(注意由于读取到的数据可能是“沾包”的,因此可能有不只一条消息)并调用ProcessCommand方法来解析和处理消息。如果处理完所有消息,则会把整个接收缓冲区重新清空。如果处理完所有消息之后还要长连接,或者buffer的最后一个字符不是消息结束符号,我们继续异步读取下一个buffer。

这里ProcessCommand方法的具体内容我就不写代码了。讲解一下。它解析传进来的ln字符串,使用Json反序列方法变为“系统消息”对象,然后调用这个对象的“执行命令”方法。而我们从参数中传入的session.Client参数,可能被用来获取客户端EndPoint,另外假设是长连接通讯模式则session.Client可能用来从消息内容中找到用户登录信息并记录下此用户匹配的socket连接(这样就可以主动推送消息给任意在线用户)。在服务器处理了命令,会把返回对象再打包成为通讯对象,然后使用Json序列化,然后直接写到session.Client中。如果是短连接,还会直接关闭这个连接,因为客户端其它的连接不会再用它来通讯,而是创建新的accept。

然而上述ProcessCommand内部其实应该异步处理命令,既而不是等ln字符串中的命令被处理完了才返回,而是直接让这个方法立刻返回。在设计通讯信令时,上面已经说了,要为长连接的消息设计SID消息编号表示。这样,即使异步返回消息处理结果,客户端收到的返回中有一个SID号对应了消息编号,所以客户端可以连续向服务器发送许多消息,而收到的返回消息再一一对应、识别是哪一个请求的,加之服务器端也是异步并发处理ProcessCommand的,于是达到了在长连接时异步并发处理客户端消息的效果。
fanbingyuan 2011-02-01
  • 打赏
  • 举报
回复
大牛们的这些理论,我看着都心跳加速了。。。
瑾安 2011-02-01
  • 打赏
  • 举报
回复
http://blog.csdn.net/xiarenwang/archive/2010/08/03/5785576.aspx
建议你看下上面的链接,总共5片,很详细,里面那个定制消息格式或许就是你想要的
瑾安 2011-02-01
  • 打赏
  • 举报
回复
利用广播
  • 打赏
  • 举报
回复
呵呵,可能我编写的所有服务器程序都是异步Accept和异步读数据的吧,我从来没有遇到过“如果你不去轮询客户端列表,你怎么知道有内容发送过来了”这个问题。异步编程不需要担心这个,因为当一个socket有数据读到,就会有一个系统线程运行我们注册的回调了,我们就在这个回调方法中处理这个客户端的请求,完全是并发的,谈不上给客户端排队处理的问题。
  • 打赏
  • 举报
回复
[Quote=引用 1 楼 bdmh 的回复:]
,接受也是,循环这个列表,然后select,看是否有可接受的内容,根本不会发生错乱
[/Quote]
服务器在accept的时候得到客户端的socket,之后就可以开始异步读,这样就不需要循环。而且.net的异步操作封装了IOCP,我们就无需只艳羡IOCP这个概念,直接使用.net的异步读操作就行了。
bdmh 2011-02-01
  • 打赏
  • 举报
回复
不对,如果一个服务端在处理一个消息的时间比较长,而这时已经有更多的客户端发来了请求,如果你不去轮询客户端列表,你怎么知道有内容发送过来了,特别对于非阻塞方式的传输

我说了,现在的模型有很多种,有类似事件通知的,这个或许可以达到所谓的‘即时’收发
  • 打赏
  • 举报
回复
当然啦,长连接还有一个特别的好处,就是不需要打洞就可以几乎实时地给不同局域网内部的客户端转发消息。
  • 打赏
  • 举报
回复
[Quote=引用 6 楼 bdmh 的回复:]

引用 4 楼 wuyq11 的回复:
c/s服务器端不需要保存客户端socket,服务器只所以返回客户端信息,都是因为客户端首先访问服务器,而服务器能够accept到客户端连接并接收数据,accept操作本身就返回了客户端socket,程序在接受一个请求之后计算出结果就直接用这个socket返回信息就可以了,不需要再额外去找其它的什么socket

这个根本处理不了大量并发问题
[/Quote]

如果结合lz的问题的根源,那么短连接恰好是可以处理大量并发的,而长连接不是。因为客户端可以(随机绑定不同的本地端口而)并发发送几十个消息,而服务器都是作为独立的会话来accept,然后读取消息内容,然后回复消息,然后就丢弃socket链接,所以无需保存复用为client端建立的socket。

lz所认知的长连接机制,他由于还是认为是“一问一答”的方式顺序处理消息的,于是这种长连接也就无法处理大量并发的情况。比如客户端发起10个消息,其中第3个消息在服务器处理需要处理100毫秒,而之后的消息处理起来只需要10毫秒,lz认为长连接机制无法处理服务器异步返回消息。

“成千上万的请求”跟“接受一个回应一个”并不矛盾,这不是选择短连接还是长连接机制的原因。用这个来判断如何编写socket编程,我是没有看到有什么标准可以参考来选择不同模型。

说到选择不同模型,其实长连接就是“长”,不管有没有通讯,都要服务器端维护客户端其socket。而短连接,客户端可以并发地发起10个独立的连接,然后哪一个链接收到回复消息就丢弃此socket(shutdown且close它)。这样在很少频繁通讯的客户端偶尔一下子需要并发许多消息,这时编程很简练。但是tcp建立一次连接(握手)实在是很慢的,因此对于需要频繁通讯的客户端也许会考虑使用长连接方式。但是长连接方式,我们就要自己做异步通信的管理,管理发送时的消息ID,对接收到的消息要找到匹配的、原来的发送消息(好正确地触发应用层的回调),需要自己管理消息超时,节省了建立tcp连接的时间而花费了(客户端和服务器端)管理socket、消息缓存和回调等管理的时间。
  • 打赏
  • 举报
回复
嗯,补充一下。在双向的连接时,服务器向客户端发送的有两种信令,可能是一个回复(其中包含之前客户端所发送的SID编号),也可能是一个push消息(通常这无需客户端回复)。
bdmh 2011-02-01
  • 打赏
  • 举报
回复
现在socket传输,已经有很多模型了,轮询制度已经不推荐了,IO端口应该是比较好的解决办法,对于小量信息的传递,可以像wuyq11 所说,接受一个回应一个,而对于成千上万的客户端请求,你可能要采取其他的模型,来提高你的数据准确性和效率了
bdmh 2011-02-01
  • 打赏
  • 举报
回复
[Quote=引用 4 楼 wuyq11 的回复:]
c/s服务器端不需要保存客户端socket,服务器只所以返回客户端信息,都是因为客户端首先访问服务器,而服务器能够accept到客户端连接并接收数据,accept操作本身就返回了客户端socket,程序在接受一个请求之后计算出结果就直接用这个socket返回信息就可以了,不需要再额外去找其它的什么socket
[/Quote]
这个根本处理不了大量并发问题
  • 打赏
  • 举报
回复
大的方面先不去讨论。针对你的问题:

[Quote=引用楼主 fanbingyuan 的回复:]
1:客户端socket异步接收,根据接收到的服务器指令完成与服务器的交互。比如客户端a向服务器发送指令x 服务器将指令x发给客户端b。疑点:异步接收如何接受。假如b先向服务器发送指令y,服务器回复指令z,但x比z先到达,会不会产生什么严重的后果。
[/Quote]
呵呵如果是短链接,那么就只有轮询了。或者如果你的服务器发现客户端是在外网可以直接访问的,可以在客户端登录时命令客户端开启一个监听服务,客户端也是一个服务器。当然,这其实增加了复杂性,但是提高了灵活性。

而当客户端无法在外网直接发起访问,可以尝试打洞。当然,这也是增加了复杂性,但是提高了灵活性。

最后才是复杂性最低的做法,长连接。这时候,服务器回复指令z之后,如果x到达了,那么服务器又回复一个新的指令了。


[Quote=引用楼主 fanbingyuan 的回复:]
12:客户端与服务器建立三个连接。一个连接保持与服务器的长连接。一个连接实现与服务器同步通信。一个连接异步接收服务器的指令,实现客户端之间交互。疑点:这样一来服务器负担翻了三番,同时也不知道如何在服务器端区分同一个客户端的三个连接那个是哪个。
[/Quote]
如果你说的是短链接,那么3个、30个都没有关系。毕竟是“短”的嘛!需要时才临时建立,然后没用了就丢弃。如果是长连接,那么长连接是双向的,无所谓为了接收创建一个、为了发送创建一个。在消息中,会有“消息ID”作为标记,比如从0一直到增长到18446744073709551615,然后再从0开始。发送消息时有SID编号,收到消息时有SID说明这是回复什么消息的。如果你要求消息回复是顺序的,就可以判断收到的回复消息号是否恰好是最后发送的SID,如果不是就可以重发SID对应的消息。如果你要求消息回复是异步的,那么就无需要求SID同步,而是匹配到刚刚发送的消息然后提交给应用层。也就是说,仅仅建立一个长连接,就可以即收也发消息,而且也可以是异步并发处理消息

[Quote=引用楼主 fanbingyuan 的回复:]
3:客户端做成一个简单服务器端,检测端口侦听来自四面八方的连接请求。疑点:客户端的ip不固定。
[/Quote]
客户端之所以为客户端,那么肯定它要先去登录到服务器。
wuyq11 2011-02-01
  • 打赏
  • 举报
回复
c/s服务器端不需要保存客户端socket,服务器只所以返回客户端信息,都是因为客户端首先访问服务器,而服务器能够accept到客户端连接并接收数据,accept操作本身就返回了客户端socket,程序在接受一个请求之后计算出结果就直接用这个socket返回信息就可以了,不需要再额外去找其它的什么socket
fanbingyuan 2011-02-01
  • 打赏
  • 举报
回复
[Quote=引用 1 楼 bdmh 的回复:]

首先你要清楚,做socket是如何发送接收数据的,你还是不清楚,服务端应该维护一个客户端连接的列表,向客户端发消息时,会循环这个列表中的socket,对没一个socket进行发送,接受也是,循环这个列表,然后select,看是否有可接受的内容,根本不会发生错乱
[/Quote]
确实不是很清楚。。。。我现在就是不知道客户端的接收应该怎么写。
ttyyadd 2011-02-01
  • 打赏
  • 举报
回复
你应该画一个流程图,这样你的思路可以更清楚一些
bdmh 2011-02-01
  • 打赏
  • 举报
回复
首先你要清楚,做socket是如何发送接收数据的,你还是不清楚,服务端应该维护一个客户端连接的列表,向客户端发消息时,会循环这个列表中的socket,对没一个socket进行发送,接受也是,循环这个列表,然后select,看是否有可接受的内容,根本不会发生错乱

110,535

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧