C/S模式下点对多。客户端是怎么接收来自服务器不定时指令的。
我想实现就是像qq弹窗那样的效果,服务器不定时向客户端发送指令,客户端却是不知如何接受。
想了几个思路,求大牛指点指点。
1:客户端socket异步接收,根据接收到的服务器指令完成与服务器的交互。比如客户端a向服务器发送指令x 服务器将指令x发给客户端b。疑点:异步接收如何接受。假如b先向服务器发送指令y,服务器回复指令z,但x比z先到达,会不会产生什么严重的后果。
2:客户端与服务器建立三个连接。一个连接保持与服务器的长连接。一个连接实现与服务器同步通信。一个连接异步接收服务器的指令,实现客户端之间交互。疑点:这样一来服务器负担翻了三番,同时也不知道如何在服务器端区分同一个客户端的三个连接那个是哪个。
3:客户端做成一个简单服务器端,检测端口侦听来自四面八方的连接请求。疑点:客户端的ip不固定。
我想实现就是像qq弹窗那样的效果,服务器不定时向客户端发送指令,客户端却是不知如何接受。
想了几个思路,求大牛指点指点。
1:客户端socket异步接收,根据接收到的服务器指令完成与服务器的交互。比如客户端a向服务器发送指令x 服务器将指令x发给客户端b。疑点:异步接收如何接受。假如b先向服务器发送指令y,服务器回复指令z,但x比z先到达,会不会产生什么严重的后果。
2:客户端与服务器建立三个连接。一个连接保持与服务器的长连接。一个连接实现与服务器同步通信。一个连接异步接收服务器的指令,实现客户端之间交互。疑点:这样一来服务器负担翻了三番,同时也不知道如何在服务器端区分同一个客户端的三个连接那个是哪个。
3:客户端做成一个简单服务器端,检测端口侦听来自四面八方的连接请求。疑点:客户端的ip不固定。
如果你说的是短链接,那么3个、30个都没有关系。毕竟是“短”的嘛!需要时才临时建立,然后没用了就丢弃。如果是长连接,那么长连接是双向的,无所谓为了接收创建一个、为了发送创建一个。在消息中,会有“消息ID”作为标记,比如从0一直到增长到18446744073709551615,然后再从0开始。发送消息时有SID编号,收到消息时有SID说明这是回复什么消息的。如果你要求消息回复是顺序的,就可以判断收到的回复消息号是否恰好是最后发送的SID,如果不是就可以重发SID对应的消息。如果你要求消息回复是异步的,那么就无需要求SID同步,而是匹配到刚刚发送的消息然后提交给应用层。也就是说,仅仅建立一个长连接,就可以即收也发消息,而且也可以是异步并发处理消息。客户端之所以为客户端,那么肯定它要先去登录到服务器。
如果结合lz的问题的根源,那么短连接恰好是可以处理大量并发的,而长连接不是。因为客户端可以(随机绑定不同的本地端口而)并发发送几十个消息,而服务器都是作为独立的会话来accept,然后读取消息内容,然后回复消息,然后就丢弃socket链接,所以无需保存复用为client端建立的socket。lz所认知的长连接机制,他由于还是认为是“一问一答”的方式顺序处理消息的,于是这种长连接也就无法处理大量并发的情况。比如客户端发起10个消息,其中第3个消息在服务器处理需要处理100毫秒,而之后的消息处理起来只需要10毫秒,lz认为长连接机制无法处理服务器异步返回消息。“成千上万的请求”跟“接受一个回应一个”并不矛盾,这不是选择短连接还是长连接机制的原因。用这个来判断如何编写socket编程,我是没有看到有什么标准可以参考来选择不同模型。说到选择不同模型,其实长连接就是“长”,不管有没有通讯,都要服务器端维护客户端其socket。而短连接,客户端可以并发地发起10个独立的连接,然后哪一个链接收到回复消息就丢弃此socket(shutdown且close它)。这样在很少频繁通讯的客户端偶尔一下子需要并发许多消息,这时编程很简练。但是tcp建立一次连接(握手)实在是很慢的,因此对于需要频繁通讯的客户端也许会考虑使用长连接方式。但是长连接方式,我们就要自己做异步通信的管理,管理发送时的消息ID,对接收到的消息要找到匹配的、原来的发送消息(好正确地触发应用层的回调),需要自己管理消息超时,节省了建立tcp连接的时间而花费了(客户端和服务器端)管理socket、消息缓存和回调等管理的时间。
建议你看下上面的链接,总共5片,很详细,里面那个定制消息格式或许就是你想要的
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的,于是达到了在长连接时异步并发处理客户端消息的效果。