之前讲解了socket应用最基础的用法以及给出了一个代码框架,本篇进一步提升一下
长链接、短连接概念:(此概念是对客户端而言的)
1、长链接就是基础篇贴出来的代码一般,即建立连接后就不断开,一直循环收发工作;
2、短链接是发送一次报文后主动断开链接,然后再建立链接再发送......(即只有在数据传输时才建立链接)
粘包概念:
因为tcp协议是流协议,数据与数据之间是没有边界的,在接收这些如流水一般的数据时不能很好地辨识这些数据而出乱子了。
注意:udp协议的数据时有边界的(即不需要考虑粘包问题)
出现粘包原因:
1、发送方引起的粘包是由TCP协议本身造成的:TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据(数据捆绑在一起从而无法解析)。
2、接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小(即在socket层我们自行分配的那个buffer)从系统接收缓冲区取数据,这样就一次取到了多包数据。
将此两点再详细化一点总结如下:
1、发送端可能会等缓冲区(tcp协议内部的缓冲区)满才会发送出去(即自动做了某些优化)
2、接收端没有及时接收数据,使得连续的两个数据包都在tcp缓存中,然后接收端再来从这个tcp缓存中接收数据(这两个数据已经粘在一起了)
什么时候要考虑粘包问题:
1、刚才上面概念所讲的短链接就不需要考虑粘包问题!(原因太简单不讲了)
2、文件传输也不需要考虑粘包问题(只是复制拷贝而已,反正全部接收到数据就是了,不管你tcp里面怎么优化、怎么延时接收/发送、都成)
3、如果要调用几次send/write而发送的数据是有结构的,那么需要考虑一下了!(比如先发送了一个请求数据,再发送了一个真实的数据,如果这两个数据被绑在一起了!)
粘包问题解决方案:
所有粘包的解决方案最本质的就是一点:加边界!(下面给出几种具体的方案)
1、定长包传输(在传输前指定要传输的数据的大小,这样就不会出错了)
2、包尾加\r\n(注:ftp协议就是这么做的)
3、其他更复杂的应用层协议(http等,或者自己写一个协议)
下面给出两个实际例子来解决粘包问题:
一、使用定长包方案(基本原理如下)
自定义一个结构体packet
struct packet //一个包:包括包头和真实数据
{
int len; //包头,里面记录了真实数据的大小
char buf[1024];//真实数据:里面就是所要发送的真实数据
};
总体步骤:(更详细见代码-----------重点是思路和readn/writen函数)
1、在发送数据前就先计算出真实数据的大小,并将大小放入packet.len中,然后将整个包packet都发送出去。
2、接收端先调用读函数读取4个字节的数据(因为发送过来的packet中的len是int型,所以是4字节)来了解到发送端真实数据的大小n;
3、接收端再调用读函数读取n大小的数据,而现在所读的数据也就是真实的数据了!
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888 //端口号,定义为宏方便以后直接修改
#define BACKLOG 10 //表服务器可以同时监测多少个客户端连接,设置为>0即可
struct packet//一个包:包括包头和真实数据(我们自己定义的结构体)
{
int len;
char buf[1024];
};
//readn(...)函数确切的可以读到count大小的数据,未读满就循环再读
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //nleft表示如果读count一次没有读完,还剩余的字节数(此处只是nleft的初始化)
ssize_t nread; //读到的字节数
char *bufp = (char*)buf;//辅助指针:若一次未读满count字节,则bufp指向buf中已有数据的末尾,相当于做的一个标记,并以这个标记继续往后读直到读满count大小
while (nleft > 0)
{ //如果进入了此循环,说明没有读满,但是buf重已有数据了,所以继续读,读的数据就紧接着放在buf中的那个标记butp处
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;//被信号中断了,就回去继续读(因为read()函数是可中断睡眠的)
return -1;
}
else if (nread == 0)//说明对方已经关闭了
return count - nleft;
bufp += nread;
nleft -= nread;
}
//能执行到这里说明nleft = 0了,即全部读完!
return count;
}
int main(int argc, char **argv)
{
int iSocketServer;
int iSocketClient;
struct sockaddr_in tSocketServerAddr;//服务器地址结构
struct sockaddr_in tSocketClientAddr;//客户端地址结构:后来当客户端来连接时会传过来
int iRet;
int n;
int iAddrLen;
struct packet recvbuf;
int iRecvLen;
int iClientNum = -1;
/*
*防止僵尸进程,子进程结束后还是会存于进程表项中,可用ps -u book(用户)查看到
*所以要发送一个信号SIGCHLD给父进程,让其给它收尸(注:所有64个信号可由kill -l查看)
*SIG_IGN为忽略的意思,可让内核把僵尸进程转交给init进程去处理,防止其占用系统资源
*/
signal(SIGCHLD,SIG_IGN);/*防止僵尸进程:子进程退出后会给父进程一个信号,然后来给它收尸即可*/
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
printf("socket error!\n");
return -1;
}
//地址复用:即若"ip地址+端口"已被使用,那么也能继续使用!
int on = 1;
if (setsockopt(iSocketServer, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
return -1;
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* 端口是2个字节即short型 */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;/*本机上所有IP,若为特定ip的话需要用inet_addr()函数来转换一下*/
memset(tSocketServerAddr.sin_zero, 0, 8);/*设置为0----8字节*/
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
/* 注:上面函数第二个参数强制类型转换为通用的sockaddr结构(因为sockaddr_in结构是) */
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
printf("listen error!\n");
return -1;
}
while (1)
{
iAddrLen = sizeof(struct sockaddr);
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);/* 参数2获得了对方的IP地址 */
if (-1 != iSocketClient)
{
iClientNum++;
printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
/*
*在父进程中调用fork()返回子进程的PID号,取非则变为0,所以直接跳过if,转到while开头继续accept新的客户端
*而子进程的fork返回0,则继续进去执行
*/
if (!fork())
{
/* 子进程 */
while (1)
{
memset(&recvbuf, 0, sizeof(recvbuf));
int ret = readn(iSocketClient, &recvbuf.len, 4); //读包头 4个字节
if (ret == -1)
return -1;
else if (ret < 4)
{
printf("client close\n");
close(iSocketClient);
return -1;
}
n = ntohl(recvbuf.len);
ret = readn(iSocketClient, recvbuf.buf, n); //根据长度读数据
if (ret == -1)
return -1;
else if (ret < n)
{
printf("client close\n");
close(iSocketClient);
return -1;
}
recvbuf.buf[ret] = '\0';
printf("Get message From Client %d: %s\n", iClientNum, recvbuf.buf);
}
}
}
}
close(iSocketServer);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888
struct packet//一个包:包括包头和真实数据(我们自己定义的结构体)
{
int len;
char buf[1024];
};
//writen()函数原理同readn()函数,不再分析
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char*)buf;
while (nleft > 0)
{
if ((nwritten = write(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;
return -1;
}
else if (nwritten == 0)
continue;
bufp += nwritten;
nleft -= nwritten;
}
return count;
}
int main(int argc, char **argv)
{
int iSocketClient;
int n;
struct sockaddr_in tSocketServerAddr;
struct packet sendbuf;
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s \n", argv[0]);//参数不为2个就打印用法
return -1;
}
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);//结构体后8位为保留位,清0
int iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
while (1)
{
if (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin))/*从stdin获得数据(我们自己实时敲入的)到sendbuf.buf*/
{
n = strlen(sendbuf.buf);//求出即将要发报文的长度
sendbuf.len = htonl(n); //将报文的长度转换为网络字节
writen(iSocketClient, &sendbuf, 4+n);//发送的报文时候,再加发4个字节的报头
}
}
return 0;
}
二、使用数据尾部加\n方案(基本原理如下)
原理:使得数据与数据之间多了一个边界,以后就可以判断这个边界从而解决粘包问题。
问:为什么引出方案二?
答:之前在方案一中已经介绍了定长包的使用,对于固定长度的数据发送来说非常方便,但是其也有应用场合的限制,例如数据长度不固定的情况!(所以引出了方案二)
总体步骤:(详见代码)
1、客户端使用fgets()函数获取键盘输入(此函数特点是数据结尾会带上\n)
2、接收端使用redline()函数来解析数据边界并将真实数据给提取出来(readline函数是关键)
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888 //端口号,定义为宏方便以后直接修改
#define BACKLOG 10 //表服务器可以同时监测多少个客户端连接,设置为>0即可
ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count; //nleft表示如果读count一次没有读完,还剩余的字节数(此处只是nleft的初始化)
ssize_t nread; //读到的字节数
char *bufp = (char*)buf;//辅助指针:若一次未读满count字节,则bufp指向buf中已有数据的末尾,相当于做的一个标记,并以这个标记继续往后读直到读满count大小
while (nleft > 0)
{ //如果进入了此循环,说明没有读满,但是buf重已有数据了,所以继续读,读的数据就紧接着放在buf中的那个标记butp处
if ((nread = read(fd, bufp, nleft)) < 0)
{
if (errno == EINTR)
continue;//被信号中断了,就回去继续读(因为read()函数是可中断睡眠的)
return -1;
}
else if (nread == 0)//说明对方已经关闭了
return count - nleft;
bufp += nread;
nleft -= nread;
}
//能执行到这里说明nleft = 0了,即全部读完!
return count;
}
//从指定的socket中预读取指定大小的数据(不会取出)
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while (1)
{
//MSG_PEEK标志:读取队列中指定大小的数据,但不取出
int ret = recv(sockfd, buf, len, MSG_PEEK);
//如果被信号中断,则继续
if (ret == -1 && errno == EINTR)
continue;
return ret;
}
}
//readline()函数用来解析数据边界并提取真实数据
ssize_t readline(int sockfd, void *buf, size_t maxline)
{
int ret;
int nread;//成功预读取的数据大小
char *bufp = buf;//辅助指针
int nleft = maxline;//maxline是封包最大值,nleft表示还有多少未读取
while (1)
{
//看一下缓冲区有没有数据(只是预读),并不移除内核缓冲区数据
ret = recv_peek(sockfd, bufp, nleft);
if (ret < 0) //失败
return ret;
else if (ret == 0) //对方已关闭
return ret;
nread = ret;
int i;
for (i = 0; i < nread; i++)
{
if (bufp[i] == '\n') //若缓冲区有\n
{
ret = readn(sockfd, bufp, i + 1); //读走数据
if (ret != i + 1)
printf("readn error\n");
return ret; //因为有\n了,说明已经读到了一个完整的数据包,那么就直接返回了!
}
}
if (nread > nleft) //如果读到的数大于 一行最大数 异常处理
printf("error:read more than maxline\n");
//能执行到此说明这一次循环中没有\n,那么接下来就把nread大小的数据真正的读出来放在buf里去(之前的recv_peek仅仅是预读取,不是真正的读!)
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if (ret != nread)
{
printf("client close\n");
close(sockfd);
return -1;
}
bufp += nread; //bufp指针后移后,再接着循环,直到遇见\n,那么才说明是\n之前的才是一个完整的数据包
}
return -1;
}
int main(int argc, char **argv)
{
int iSocketServer;
int iSocketClient;
struct sockaddr_in tSocketServerAddr;//服务器地址结构
struct sockaddr_in tSocketClientAddr;//客户端地址结构:后来当客户端来连接时会传过来
int iRet;
int n;
int iAddrLen;
int iRecvLen;
int iClientNum = -1;
/*
*防止僵尸进程,子进程结束后还是会存于进程表项中,可用ps -u book(用户)查看到
*所以要发送一个信号SIGCHLD给父进程,让其给它收尸(注:所有64个信号可由kill -l查看)
*SIG_IGN为忽略的意思,可让内核把僵尸进程转交给init进程去处理,防止其占用系统资源
*/
signal(SIGCHLD,SIG_IGN);/*防止僵尸进程:子进程退出后会给父进程一个信号,然后来给它收尸即可*/
iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
printf("socket error!\n");
return -1;
}
//地址复用
int on = 1;
if (setsockopt(iSocketServer, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
return -1;
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* 端口是2个字节即short型 */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;/*本机上所有IP,若为特定ip的话需要用inet_addr()函数来转换一下*/
memset(tSocketServerAddr.sin_zero, 0, 8);/*设置为0----8字节*/
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
/* 注:上面函数第二个参数强制类型转换为通用的sockaddr结构(因为sockaddr_in结构是) */
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
printf("listen error!\n");
return -1;
}
while (1)
{
iAddrLen = sizeof(struct sockaddr);
iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);/* 参数2获得了对方的IP地址 */
if (-1 != iSocketClient)
{
iClientNum++;
printf("Get connect from client %d : %s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
/*
*在父进程中调用fork()返回子进程的PID号,取非则变为0,所以直接跳过if,转到while开头继续accept新的客户端
*而子进程的fork返回0,则继续进去执行
*/
if (!fork())
{
/* 子进程 */
char recvbuf[1024];//给真实数据分配一块缓存
while (1)
{
memset(recvbuf, 0, sizeof(recvbuf));
int ret = readline(iSocketClient, recvbuf, 1024);
if (ret == -1)
printf("readline error\n");
if (ret == 0)
{
printf("client close\n");
close(iSocketClient);
}
recvbuf[ret] = '\0';
printf("Get message From Client %d: %s\n", iClientNum, recvbuf);
//或者用:fputs(recvbuf, stdout);
}
}
}
}
close(iSocketServer);
return 0;
}
client.c
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
unsigned char ucSendBuf[1000];//发送缓冲区
int iSendLen;
if (argc != 2)
{
printf("Usage:\n");
printf("%s \n", argv[0]);//参数不为2个就打印用法
return -1;
}
iSocketClient = socket(AF_INET, SOCK_STREAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);//结构体后8位为保留位,清0
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
while (1)
{
if (fgets(ucSendBuf, 999, stdin))/*从stdin获得数据(我们自己实时敲入的)到ucSendBuf*/
{
iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
if (iSendLen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
return 0;
}
两种解决粘包的方案分析完毕!
再次总结粘包的办法就是三个字:加边界