流式套接字基本使用
在一个应用程序创建套接字进行通信时,socket()中的通信类型参数会让用户从数据流、数据报、原始类型中作出选择。其中数据流的传输方式就是基于TCP,而TCP因其数据流、可靠的特点适用于大多数情况。
流式套接字特点及适用情况
流式套接字基于传输控制协议提供面向连接的、可靠的(但也会比UDP付出更多传输代价的)数据流传输服务。
因其保证数据能无错无重发出、有序接收的可靠性,在可靠性要求高的传输中适用。(避免了使用不可靠传输的维护编码代价)
又因其流式传输的特点,在大数据量的传输中适用。(避免了极高的传输代价)
流式套接字的通信过程
(上一章有说过)
服务器:①创建、绑定、监听流式套接字 ②等待到客户请求并接收(TCP的三次握手) ③数据收发
④断开连接(TCP的四次挥手) ⑤关闭套接字 ⑥关闭监听套接字或回到2
客户端:①创建套接字 ②发送请求给服务器( ) ③数据收发 ④断开连接( ) ⑤关闭套接字
注:服务器在收到客户请求后应创建一个新的套接字用于连接,原套接字仍进行监听 (上一章有说过)。
在循环服务器中监听函数会作为一个队列储存连接期间其他客户端所发送的请求,服务器会循环进行②到⑥。
在并发服务器中,服务器在收到客户请求后会创建一个单独的子进程或线程,然后在该线程里创建一个新的套接字用于与客户端通信。而原套接字仍能正常的接收客户端的请求并回应。
TCP的流传输控制:
TCP传输是以一种字节流的形式传输,不存在边界的概念。所以在传输过程中程序A给程序B发送消息时,程序B并不清楚数据发送的真实情况。(TCP协议会根据网络状况决定数据发送的字节)
当程序A希望向程序B传输M1,M2两条消息时。理想情况就是M1,M2消息依次到达。但在实际的网络环境中信息可能会因为TCP协议的限制、网络状况的影响导致信息发送延迟、信息被分成多次发送、信息合并发送、甚至数据传输过程出现错误。
为了避免数据接收出现问题,在接收的过程中应考虑循环接收。
循环接收的简单实现(当recv()函数成功接收数据会返回接收的长度,若返回为0则表示连接已关闭,返回为负则出错)
int iResult, recvbuflen = N;char recvbuf [N]; //接收缓冲区do{iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);if(iResult>0){//正常接收,继续接收}else {if(iResult==0){//连接已断开}else//iResult < 0,传输过程出现问题}}(iResult>0);
通过循环接收的方法可以使接收方在不清楚数据大小的情况下接收流式数据,避免缓冲区的浪费或溢出,并能在每次接收时都考虑到接收出错情况。
上面的循环接收可以保证完整的接收数据流。但TCP传输多次进行时,接收方还需要考虑到数据流的分割问题。即不能仅靠连接关闭作为接收结束的标准,而是将数据流以数据包的形态接收,确保多次请求响应传输的数据不会混淆。因此流式套接字接收存在以下常见的两种方法:
1、接收固定长度的消息
接收定长数据的情况一般是接收方已知接收数据的长度,因此接收结束的条件可以是连接关闭或接收到相应长度的数据。
接收函数仍使用循环接收。但相比上面简单的实现,接收方应增加一个所接收数据的长度变量,剩余接收数据长度变量。循环的标准不再是正常接收则循环,而是当有剩余接收的数据就继续循环,直到全部接收、循环结束、函数结束。或是接收过程中出现传输错误或连接断开的情况,返回信息、函数结束。注意,在每次循环后都要将接收缓冲区向后移动已接收的长度,并更新剩余接收数据。
代码略
2、接收可变长度的消息
相比于固定长度的数据接收,接收数据长度是可变更为常见且更令人头疼 。面对这种情况,一般有两种解决方法。
1、用结束标记来分割变长消息。(不太可能会用的)
这种方法通过结束标记来表示一段传输数据的结束。理解起来很简单,但在实际实现过程中需要考虑到数据中出现结束字符的情况,因此会需要转义字符。转义字符在数据中仍会出现,因此还需要将转义字符替换。对接收方而言,需要将整个消息先扫描一次替换转义字符,再扫描一次识别结束字符。在实际实现中效率低下,数据可能会非常臃肿,实现起来也十分麻烦。
2、用长度字段标记变长消息长度
这种方法在网络协议中十分常见,通过在传输数据前添加一个消息长度的首部使接收方可以清楚的知道所接收数据的长度,在传输顺利的情况下与定长接收类似。
变长接收的代码:
//定长接收的函数为recvn(SOCKET s, char* recvbuf, unsigned int fixedlen);int recvvl(SOCKET s, char* recvbuf, unsigned int recvbuflen){int iResult;unsigned int reclen;iResult = recvn(s, (char *)&reclen, sizeof(unsigned int)); //获取接收数据的长度if(iResult != sizeof(unsigned int)){ //未能成功获取的情况if(iResult == -1)return -1; //传输错误elsereturn 0; //连接关闭}reclen = ntohl(reclen); //将数据长度信息转化为主机顺序if(reclen > recvbuflen){ //缓冲区不足,接收消息并丢弃,返回错误while(reclen > 0){ //当此循环再次执行时,上次接收的数据会被覆盖而丢弃iResult = recvn(s, recvbuf, recvbuflen);if(iResult != recvbuflen){if(iResult == -1)return -1; //传输错误elsereturn 0; //连接关闭}reclen=-recvbuflen; //更新剩余传输数据长度if(reclen < recvbuflen)recvbuflen = reclen; //修改为最后一段数据的长度}return -1; //数据超出缓冲区}//当以上情况都未发生,则正常接收长度为reclen的数据iResult = recvn(s, recvbuf, reclen);if(iResult != reclen){if(iResult == -1)return -1; //传输出错elsereturn 0; //连接关闭}return iResult; //成功接收数据}
注:以上代码有待优化