国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > 互联网 > Socket编程实践(6) --TCP服务器常见问题(1)

Socket编程实践(6) --TCP服务器常见问题(1)

来源:程序员人生   发布时间:2014-12-17 08:21:14 阅读次数:2649次

流协议与粘包

粘包的表现

Host A 发送数据给 Host B; 而Host B 接收数据的方式不肯定


 



粘包产生的缘由 

 



说明

TCP

字节流,无边界

对等方,1次读操作,不能保证完全把消息读完

UDP

数据报,有边界

对方接受数据包的个数是不肯定的

 

 

产生粘包问题的缘由分析

    1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)

    2、tcp传送的端 mss大小限制

    3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,致使消息分割。

    4、tcp的流量控制和堵塞控制,也可能致使粘包

    5、tcp延迟发送机制等

结论:tcp/ip协议,在传输层没有处理粘包问题。

 

粘包解决方案(本质上是要在利用层保护消息与消息的边界)

  定长包

  包尾加 (ftp)

  包头加上包体长度(以下)

  更复杂的利用层协议

 

编程实践-readn && writen

管道,FIFO和某些装备(特别是终端和网络)有以下两种性质:

    1)1次read操作所返回的数据可能少于所要求的数据,即便还没到达文件尾端也可能这样,但这不是1个毛病,应当继续读该装备;

    2)1次write操作的返回值也可能少于指定输入的字节数.这多是由于某个因素酿成的,如:内核缓冲区满...但这也不是1个毛病,应当继续写余下的数据(通常,只有非阻塞描写符,或捕捉到1个信号时,才产生这类write的中途返回)

     在读写磁盘文件时从未见到过这类情况,除非是文件系统用完了空间,或接近了配额限制,不能将所要求写的数据全部写出!

 

   通常,在读,写1个网络装备,管道或终端时,需要斟酌这些特性.因而,我们就有了下面的这两个函数:readn和writen,功能分别是读写指定的N字节数据,并处理返回值可能小于要求值的情况:

ssize_t readnint fd, void *buf, size_t count); ssize_t writen(int fd, const void *buf, size_t count);


返回值:

    读写的字节数;若出错,返回⑴

 

实现:

    这两个函数只是按需屡次调用read和write系统调用直至读写了N个数据

ssize_t readn(int fd,void *buf,size_t count) { size_t nLeft = count; ssize_t nRead = 0; char *ptr = static_cast<char *>(buf); while (nLeft > 0) { if ((nRead = read(fd,ptr,nLeft)) < 0) { //1点东西都没读 if (nLeft == count) { return ⑴; //error } else { break; //error, return amount read so far } } else if (nRead == 0) { break; //EOF } nLeft -= nRead; ptr += nRead; } return count - nLeft; } ssize_t writen(int fd, const void *buf, size_t count) { size_t nLeft = count; ssize_t nWritten; const char *ptr = static_cast<const char *>(buf); while (nLeft > 0) { if ((nWritten = write(fd,ptr,nLeft)) < 0) { //1点东西都没写 if (nLeft == count) { return ⑴; //error } else { break; //error, return amount write so far } } else if (nWritten == 0) { break; //EOF } nLeft -= nWritten; ptr += nWritten; } return count - nWritten; }

报头加上报文长度编程实践

报文结构:

struct TransStruct { int m_length; //报头:保存数据m_text的真实数据长度 char m_text[BUFSIZ]; //报文:保存真正要发送的数据 };

 

   发报文时:前4个字节长度+报文

   收报文时:先读前4个字节,求出长度;根据长度读数据。

//server端完全代码及解析 #include "commen.h" //echo 服务器writen,readn 版 int main() { int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd == ⑴) { err_exit("socket error"); } //添加地址复用 int optval = 1; if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == ⑴) { err_exit("setsockopt SO_REUSEADDR error"); } //绑定 struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8002); serverAddr.sin_addr.s_addr = INADDR_ANY; //绑定本机的任意1个IP地址 if (bind(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == ⑴) { err_exit("bind error"); } //启动监听套接字 if (listen(sockfd,SOMAXCONN) == ⑴) { err_exit("listen error"); } struct sockaddr_in peerAddr; socklen_t peerLen = sizeof(peerAddr); while (true) { //接受链接 int peerSockfd = accept(sockfd, (struct sockaddr *)&peerAddr,&peerLen); if (peerSockfd == ⑴) { err_exit("accept error"); } //打印客户信息 cout << "Client:" << endl; cout << " sin_port: " << ntohs(peerAddr.sin_port) << endl; cout << " sin_addr: " << inet_ntoa(peerAddr.sin_addr) << endl; cout << " socket: " << peerSockfd << endl; //每有1个客户端连接进来,就fork1个子进程, //相应的业务处理由子进程完成,父进程继续监听 pid_t pid = fork(); if (pid == ⑴) { close(sockfd); close(peerSockfd); err_exit("fork error"); } else if (pid == 0) //子进程,处理业务 { close(sockfd); //子进程关闭监听套接字,由于子进程不负责监听凭务 struct TransStruct recvBuf; ssize_t readCount = 0; while (true) { memset(&recvBuf,0,sizeof(recvBuf)); //首先,从客户端读取报头长度 if ((readCount = readn(peerSockfd,&(recvBuf.m_length),4)) == ⑴) { err_exit("readn error"); } else if (readCount == 0) //如果链接关闭 { peerClosePrint("client connect closed"); } //根据报文实际长度,读取数据 if ((readCount = readn(peerSockfd,&(recvBuf.m_text),recvBuf.m_length)) == ⑴) { err_exit("readn error"); } else if (readCount == 0) { peerClosePrint("client connect closed"); } //将整体报文回写回客户端 if (writen(peerSockfd,&recvBuf,recvBuf.m_length+4) == ⑴) { err_exit("writen error"); } recvBuf.m_text[recvBuf.m_length] = 0; //写至终端 fputs(recvBuf.m_text,stdout); } } else if (pid > 0) //父进程 { close(peerSockfd); } } close(sockfd); return 0; }

//client端完全代码实现及解析 #include "commen.h" int main() { int sockfd = socket(AF_INET,SOCK_STREAM,0); if (sockfd == ⑴) { err_exit("socket error"); } //填写好服务器地址及其端口号 struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8002); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == ⑴) { err_exit("connect error"); } int readCount = 0; struct TransStruct sendBuf; struct TransStruct recvBuf; //从键盘输入数据 while (fgets(sendBuf.m_text,sizeof(sendBuf.m_text),stdin) != NULL) { //保存的是真实报文的长度 sendBuf.m_length = strlen(sendBuf.m_text); //向server发送数据....+4的缘由:需要添加报首的4个字节报头的长度 if (writen(sockfd,&sendBuf,sendBuf.m_length+4) == ⑴) { err_exit("write socket error"); } //首先,从server端接收将要发送的数据报的长度 if ((readCount = readn(sockfd,&(recvBuf.m_length),4)) == ⑴) { err_exit("read socket error"); } else if (readCount == 0) { peerClosePrint("client connect closed"); } //然后,根据从server端读来的报文长度,读取报文 if ((readCount = readn(sockfd,&(recvBuf.m_text),recvBuf.m_length)) == ⑴) { err_exit("read socket error"); } else if (readCount == 0) { peerClosePrint("client connect closed"); } recvBuf.m_text[recvBuf.m_length] = 0; //将其回写到终端 fputs(recvBuf.m_text,stdout); memset(&sendBuf,0,sizeof(sendBuf)); memset(&recvBuf,0,sizeof(recvBuf)); } close(sockfd); return 0; }

-commen.h完全代码及解析

#ifndef COMMEN_H_INCLUDED #define COMMEN_H_INCLUDED #include <unistd.h> #include <signal.h> #include <errno.h> #include <fcntl.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/msg.h> #include <sys/sem.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <iostream> using namespace std; //报文结构 struct TransStruct { int m_length; //报头:保存数据m_text的真实数据长度 char m_text[BUFSIZ]; //报文:保存真正要发送的数据 }; //出错退出 void err_exit(std::string str) { perror(str.c_str()); exit(EXIT_FAILURE); } //对端关闭链接退出 void peerClosePrint(std::string str = "peer connect closed") { cout << str << endl; _exit(0); } //信号捕获函数:上1篇博客中的代码需要使用的 void onSignal(int signalNumber) { switch (signalNumber) { case SIGUSR1: cout << "child receive SIGUSR1" << signalNumber << endl; _exit(0); case SIGUSR2: cout << "parent receive SIGUSR2: " << signalNumber << endl; _exit(0); default: cout << "RECV OTHRER SIGNAL" << endl; } } //经典的readn函数(来源:APUE) ssize_t readn(int fd,void *buf,size_t count) { size_t nLeft = count; ssize_t nRead = 0; char *ptr = static_cast<char *>(buf); while (nLeft > 0) { if ((nRead = read(fd,ptr,nLeft)) < 0) { //1点东西都没读 if (nLeft == count) { return ⑴; //error } else { break; //error, return amount read so far } } else if (nRead == 0) { break; //EOF } nLeft -= nRead; ptr += nRead; } return count - nLeft; } //经典的writen函数(来源:APUE) ssize_t writen(int fd, const void *buf, size_t count) { size_t nLeft = count; ssize_t nWritten; const char *ptr = static_cast<const char *>(buf); while (nLeft > 0) { if ((nWritten = write(fd,ptr,nLeft)) < 0) { //1点东西都没写 if (nLeft == count) { return ⑴; //error } else { break; //error, return amount write so far } } else if (nWritten == 0) { break; //EOF } nLeft -= nWritten; ptr += nWritten; } return count - nWritten; } #endif // COMMEN_H_INCLUDED

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生