1、流协议与粘包
TCP协议是基于字节流的,数据无边界,无边界反应在对方接收消息时无法保证一次性返回多少个字节,有可能收到的不是一个完整的消息,可能是半个消息、或者多于一个消息,这就是TCP的粘包问题
UDP协议基于数据报,数据有边界,体现在对方一次收到的是一个完整的消息
产生粘包的因素
如上图所示,应用层首先用write函数将要发送的数据拷贝到套接字发送缓冲区中,该缓冲区的大小由SO_SNDBUF来指定,如果应用程序缓冲区的大小超过了套接字发送缓冲区大小,这时候消息就会被分割,第一部分消息从发送缓冲区中发送出去后,后面的数据才进去发送缓冲区,这样对方在接收数据时,就先收到数据的一部分,另一部分就有延时,这样粘包问题就产生了;
再者,TCP传输层协议传输的数据段有最大段MSS的限制,数据如果太大,那么会被分割为MSS大小的数据节,这也会导致粘包问题;
或者,数据链路层有最大传输单元MTU的限制,如果数据超过了MTU,那么在IP就要对数据进行分组(分片),这也会造成粘包问题!另外,TCP的流量控制,拥塞控制,延迟发送机制,都可能会造成粘包问题!
2、解决粘包问题
既然TCP没有自己维护数据的边界,那么就需要我们自己在应用层维护!方法有几种:
- 定长包
- 包尾加上\r\n (FTP协议就这么干的)
- 包头指明包的数据长度
- 设计更复杂的应用层协议
一些问题:
如果采用定长包,那么两方通信会很浪费带宽,比如:定长定为1024字节,而实际的数据只有10个字节,而还是会填充成1024个字节去发送,这就浪费了1014个字节,因此最合适的方法还是在包头指明包的数据长度,这样灵活高效!
3、代码实现
公用函数的定义
#define handle_error(msg) \
do{perror(msg);exit(EXIT_FAILURE);}while(0)
//包结构的定义
typedef struct package
{
int len; //包实际的数据长度
char buf[1024]; //包的数据存储区
}Package;
//接收指定数目的数据
ssize_t readn(int fd, void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩余字节数
ssize_t nread = 0; //已读字节数
char *pbuf = (char*)buf;
while(nleft > 0)
{
if((nread = read(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if (nread == 0)
return count - nleft;
pbuf += nread;
nleft -= nread;
}
return count;
}
//发送指定数目的数据
ssize_t writen(int fd, const void *buf, size_t count)
{
if((fd < 0) || (buf == NULL) || (count < 0))
return -1;
size_t nleft = count; //剩余字节数
ssize_t nwritten = 0; //已发送字节数
char *pbuf = (char*)buf;
while (nleft > 0)
{
if((nwritten = write(fd, pbuf, nleft)) < 0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(nwritten == 0)
continue;
pbuf += nwritten;
nleft -= nwritten;
}
return count;
}
服务端代码
server.c
编译命令:gcc -Wall -g -std=gnu99 server.c -o server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
//忽略子进程的SIG_CHLD信号
void handle_SIGIGN(void)
{
struct sigaction act_chld;
act_chld.sa_handler = SIG_IGN;
act_chld.sa_flags = 0;
sigemptyset(&act_chld.sa_mask);
if(-1 == sigaction(SIGCHLD, &act_chld, NULL))//捕获终端中断信号
handle_error("sigaction SIGCHLD");
}
void do_work(int sock);
int main(void)
{
handle_SIGIGN();
int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
//使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
int on = 1;
if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
close(sk_fd);
handle_error("setsockopt");
}
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);
if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("bind");
}
//被动套接字
if(listen(sk_fd, SOMAXCONN) < 0) //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
{
close(sk_fd);
handle_error("listen");
}
struct sockaddr_in cl_addr;
socklen_t cl_length = sizeof(cl_addr);
pid_t pid;
while (true)
{
memset(&cl_addr,0,sizeof(cl_addr));
int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
if(ac_sk < 0)
{
if(errno == EINTR)
continue;
close(sk_fd);
handle_error("accept");
}
printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));
//每个客户端对应一个子进程
pid = fork();
if(pid == -1)
handle_error("fork");
if(pid == 0)
{
close(sk_fd);
do_work(ac_sk);
close(ac_sk);
exit(EXIT_SUCCESS);
}
else
close(ac_sk);
}
close(sk_fd);
return 0;
}
void do_work(int sock)
{
Package recvbuf;
while(true)
{
memset(&recvbuf,0,sizeof(recvbuf));
int iret = readn(sock,&recvbuf.len,sizeof(recvbuf.len)); //获取包数据长度
if(iret == -1)
handle_error("read");
else if(iret < sizeof(recvbuf.len))
{
printf("Client was closed!\n");
break;
}
int n = ntohl(recvbuf.len); //网络字节序转换为主机字节序
iret = readn(sock, recvbuf.buf, n); //正式接收数据
if(iret == -1)
handle_error("read");
else if(iret < n)
{
printf("Client was closed!\n");
break;
}
printf("recv = %d\n", n);
fputs(recvbuf.buf,stdout);
writen(sock, &recvbuf, sizeof(recvbuf.len) + n); //回传数据
}
}
客户端代码
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
int main(void)
{
int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(sk_fd < 0)
handle_error("socket");
struct sockaddr_in sr_addr;
memset(&sr_addr,0,sizeof(sr_addr));
sr_addr.sin_family = AF_INET;
sr_addr.sin_port = htons(5188);
//sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&sr_addr.sin_addr);
if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
{
close(sk_fd);
handle_error("connect");
}
Package sendbuf;
Package recvbuf;
while (fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL)
{
int n = strlen(sendbuf.buf);
sendbuf.len = htonl(n); //主机字节序转换为网络字节序
writen(sk_fd, &sendbuf, sizeof(sendbuf.len) + n); //发送数据
memset(&sendbuf, 0, sizeof(sendbuf));
memset(&recvbuf, 0, sizeof(recvbuf));
int iret = readn(sk_fd, &recvbuf.len, sizeof(recvbuf.len)); //接收包数据长度
if(iret == -1)
handle_error("read");
else if(iret < sizeof(recvbuf.len))
{
printf("Server was closed!\n");
break;
}
n = ntohl(recvbuf.len); //网络字节序转换为主机字节序
iret = readn(sk_fd, recvbuf.buf, n); //正式接收数据
if(iret == -1)
handle_error("read");
else if(iret < n)
{
printf("Server was closed!\n");
break;
}
printf("recv = %d\n", n);
fputs(recvbuf.buf, stdout);
}
close(sk_fd);
return 0;
}