主要内容:

1、read,write 与 recv,send函数。 recv函数只能用于套接口IO
  ssize_t recv(int sockfd,void * buff,size_t len,int flags)
  ssize_t send(int sockfd,const void *buff,size_t len,int flags)
  flags为0或者为常值的或 MSG_OOB:发送或接收带外数据 (紧急数据)
               MSG_PEEK:窥看外来消息(接收缓冲区数据,但并不将缓冲区数据清除) 2、readline函数实现
3、用readline实现回射客户/服务器 。利用readline解决粘包问题

  客户端程序:

  1 //利用readline函数解决粘包问题,每次读取一行。\n换行
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<string.h>
6 #include<stdlib.h>
7 #include<stdio.h>
8 #include<errno.h>
9 #include<netinet/in.h>
10 #include<arpa/inet.h>
11 #include<signal.h>
12 #define ERR_EXIT(m)\
13 do\
14 {\
15 perror(m);\
16 exit(EXIT_FAILURE);\
17 }while(0)
18 ssize_t readn(int fd,void *buf,size_t count)
19 {
20 size_t nleft=count;
21 ssize_t nread;
22 char *bufp=(char*)buf;
23 while(nleft>0)
24 {
25 if((nread=read(fd,bufp,nleft))<0)
26 {
27 if(errno==EINTR)
28 continue;
29 else
30 return -1;
31 }
32 else if(nread==0)
33 return (count-nleft);
34 bufp+=nread;
35 nleft-=nread;
36 }
37 return count;
38 }
39 ssize_t writen(int fd, const void *buf, size_t count)
40 {
41 size_t nleft=count;
42 ssize_t nwritten;
43 char *bufp=(char*)buf;
44 while(nleft>0)
45 {
46 if((nwritten=write(fd,bufp,nleft))<0)
47 {
48 if(errno==EINTR)
49 continue;
50 return -1;
51 }else if(nwritten==0)
52 continue;
53 bufp+=nwritten;
54 nleft-=nwritten;
55 }
56 return count;
57
58 }
59 ssize_t recv_peek(int sockfd,void *buf,size_t len)
60 {
61 while(1)
62 {
63 int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf(len是buf的长度),但不去清空sockfd,偷窥
64 if(ret==-1&&errno==EINTR)
65 continue;//信号中断
66 return ret;
67 }
68 }
69 //偷窥方案实现readline,避免一次读取一个字符
70 ssize_t readline(int sockfd,void * buf,size_t maxline)
71 {
72 int ret;
73 int nread;
74 size_t nleft=maxline;
75 char *bufp=(char*)buf;
76 while(1)
77 {
78 ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看
79 if(ret<0)
80 return ret;
81 else if(ret==0)
82 return ret;
83 nread=ret;
84 int i;
85 for(i=0;i<nread;i++)
86 {
87 if(bufp[i]=='\n')
88 {
89 ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空
90 if(ret!=i+1)
91 exit(EXIT_FAILURE);
92 return ret;
93 }
94 }
95 if(nread>nleft)
96 exit(EXIT_FAILURE);
97 nleft-=nread;
98 ret=readn(sockfd,bufp,nread);
99 if(ret!=nread)
100 exit(EXIT_FAILURE);
101 bufp+=nread;//移动指针继续窥看
102 }
103 return -1;
104 }
105 int main(void)
106 {
107 int sock;//客户端创建套接字
108 if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
109 ERR_EXIT("socket error");
110
111 struct sockaddr_in servaddr;//本地协议地址赋给一个套接字
112 memset(&servaddr,0,sizeof(servaddr));
113 servaddr.sin_family=AF_INET;
114 servaddr.sin_port=htons(5188);
115
116 servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器段地址
117 //inet_aton("127.0.0.1",&servaddr.sin_addr);
118
119 if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
120 ERR_EXIT("connect");
121
122 //利用getsockname获取客户端本身地址和端口,即为对方accept中的对方套接口
123 struct sockaddr_in localaddr;
124 socklen_t addrlen=sizeof(localaddr);//需要初始化
125 if(getsockname(sock,(struct sockaddr *)&localaddr,&addrlen)<0)
126 ERR_EXIT("getsockname error");
127 printf("local IP=%s, local port=%d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));
128 //使用getpeername获取对方地址
129
130 char sendbuf[1024]={0};
131 char recvbuf[1024]={0};
132 while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)//默认有换行符
133 {
134 writen(sock,sendbuf,strlen(sendbuf));//对方通过\n来分割
135 int ret=readline(sock,recvbuf,1024);
136 if(ret==-1)
137 ERR_EXIT("readline");
138 else if(ret==0)
139 {
140 printf("service closed\n");
141 break;
142 }
143 fputs(recvbuf,stdout);
144 memset(sendbuf,0,sizeof(sendbuf));
145 memset(recvbuf,0,sizeof(recvbuf));
146 }
147 close(sock);
148 return 0;
149 }

  服务器程序:

/*
服务端程序
*/
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nread;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
else
return -1;
}
else if(nread==0)
return (count-nleft);
bufp+=nread;
nleft-=nread;
}
return count;
}
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; }
//封装一个recv_peek函数
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
//recv只能用于套接口IO。通过flags可以指定接收选项。MSG_PEEK接收缓冲区数据但不清除sockfd数据。read会清除。
int ret=recv(sockfd,buf,len,MSG_PEEK);//从sockfd读取内容到buf,但不去清空sockfd,偷窥
if(ret==-1&&errno==EINTR)
continue;
return ret;
}
}
//偷窥方案实现readline(读取一行),避免一次读取一个字符.maxline 一行最多的字节数。
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
int ret;
int nread;
size_t nleft=maxline;//不能超过maxline
char *bufp=(char*)buf;
while(1)
{
ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窥看。接下来可以去读sockfd内容。
if(ret<0)
return ret;//失败
else if(ret==0)
return ret;//对方关闭套接口
nread=ret;
int i;
//判断缓冲区中是否有\n
for(i=0;i<nread;i++)
{
if(bufp[i]=='\n')
{
ret=readn(sockfd,bufp,i+1);//读出sockfd中的一行并且清空sockfd这一行
if(ret!=i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//查看数据的时候缓冲区没有\n.还不满一条消息,先将其读出来
if(nread>nleft)
exit(EXIT_FAILURE);//字节数大于nleft(maxline)
nleft-=nread;//这行剩余最多的字节数
ret=readn(sockfd,bufp,nread);//这些字符先从sockfd读走(还没有遇到\n)
if(ret!=nread)
exit(EXIT_FAILURE);//readn出错
bufp+=nread;//移动recv_peek缓冲区的指针,继续窥看。
}
return -1;
}
void do_service(int conn)
{
int ret;
char recvbuf[1024];
while(1)
{
memset(&recvbuf,0,sizeof(recvbuf));
//读取一行消息
ret=readline(conn,recvbuf,1024);
//客户端关闭
if(ret==-1)
ERR_EXIT("readline");
else if(ret==0)
{
printf("client close\n");
break;//不用继续循环等待客户端数据
}
fputs(recvbuf,stdout);
writen(conn,recvbuf,strlen(recvbuf));
}
}
int main(void)
{
int listenfd;
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
ERR_EXIT("socket error");
//if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) //本地协议地址赋给一个套接字
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//表示本机地址
//servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");
//inet_aton("127.0.0.1",&servaddr.sin_addr); //开启地址重复使用,关闭服务器再打开不用等待TIME_WAIT
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt error");
//绑定本地套接字
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind error");
if(listen(listenfd,SOMAXCONN)<0)//设置监听套接字(被动套接字)
ERR_EXIT("listen error"); struct sockaddr_in peeraddr;//对方套接字地址
socklen_t peerlen=sizeof(peeraddr);
int conn;//已连接套接字(主动套接字)
pid_t pid;
while(1){
if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept error");
//连接好之后就构成连接,端口是客户端的。peeraddr是对端
printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
pid=fork();
if(pid==-1)
ERR_EXIT("fork");
if(pid==0){
close(listenfd);
do_service(conn);
//某个客户端关闭,结束该子进程,否则子进程也去接受连接
exit(EXIT_SUCCESS);
}else close(conn);
}
return 0;
}

当我们传输如文件这种数据时,流式的传输非常适合,但是当我们传输指令之类的数据结构时,流式模型就有一个问题:无法知道指令的结束。所以粘包必须问题是必须解决的

短连接

最简单的方法就是短连接,也就是需要发送数据的时候建立TCP连接,发送完一个数据包后就断开TCP连接,这样接收端自然就知道数据结束了。 
但是这样的方法因为会多次建立TCP连接,性能低下。随便用用还可以,只要稍微对性能有一点追求的人就不会使用这种方法。

长连接

使用长连接能够获得更好的性能但不可避免的会遇到如何判断数据结构的开始与结束的问题。 
而此时的处理方式根据数据结构的类型分两种方式。

定长结构

因为粘包问题的存在,接收端不能想当然的以为发送端一次发送了多少数据就能一次收到多少数据。如果发送端发送了一个固定长度的数据结构,接收端必须每次都严格判断接收到额数据的长度,当收到的数据长度不足时,需要再次接收数据,直到满足长度,当收到的数据多于固定长度时,需要截断数据,并将多余的数据缓存起来,视为长度不足需要再次接收处理。

不定长结构

定长的数据结构是一种理想的情况,真正的应用中通常使用的都是不定长的数据结构。 
对于发送不定长的数据结构,简单的做法就是选一个固定的字符作为数据包结束标志,接收到这个字符就代表一个数据包传输完成了。 
但是这只能应用于字符数据,因为二进制数据中很难确定结束字符到底是结束还是原本要传输的数据内容(使用字符来标识数据的边界在传输二进制数据时时可以实现的,只是实现比较复杂和低效。想了解可以参考以太网传输协议)。 
目前最通用的做法是在每次发送的数据的固定偏移位置写入数据包的长度。 
接收端只要一开始读取固定偏移的数据就可以知道这个数据包的长度,接下来的流程就和固定长度数据结构的处理流程类似。

所以对于处理粘包的关键在于提前获取到数据包的长度,无论这个长度是提前商定好的还是写在在数据包的开头。 
因为在每次发送的数据的固定偏移位置写入数据包的长度的方法是最通用的一种方法,所以对这种方法实现中的一些容易出错误的地方在此特别说明。

    • 通常我们使用2~4个字节来存放数据长度,多字节数据的网络传输需要注意字节序,所以要注意接受者和发送者要使用相同的字节序来解析数据长度。
    • 每次新开始接收一段数据时不要急着直接去解析数据长度,先确保目前收到的数据已经足够解析出数据长度,例如数据开头的2个字节存储了数据长度,那么一定确保接收了2个字节以上的数据后才去解析数据长度。 
      • 如果没做到这一点的服务器代码,收到了一个字节就去解析数据长度的,结果得到的长度是内存中的随机值,结果必然是崩溃的
    • 有些非法客户端或者有bug的客户端可能会发出错误的数据,导致解析出的数据长度异常的大,一定要对解析出的数据长度做检查,事先规定一个合适的长度,一旦超过果断关闭SOCKET,避免服务器无休止的等待下去浪费资源。 
      • 不要妄想说自己写的客户端不会出错,哪怕客户端不出错,只要其他任何一个使用TCP的客户端写错了端口,也足以让你崩溃,毕竟管得了自己管不了别人
    • 处理完一个完整的数据包后一定检查是否还有未处理的数据,如果有的话要对这段多余的数据再次开始解析数据长度的过程。不要忙着去继续接受数据。 
      • 这应该是最常犯的一个错误,很多人以为完整的处理了一个数据包后就万事大吉,可以重新开始处理流程,但是别忘了,收到的数据有可能带着下一个数据包的数据,别把他们忘掉

TCP粘包问题的解决方案02——利用readline函数解决粘包问题的更多相关文章

  1. 《精通并发与Netty》学习笔记(14 - 解决TCP粘包拆包(二)Netty自定义协议解决粘包拆包)

    一.Netty粘包和拆包解决方案 Netty提供了多个解码器,可以进行分包的操作,分别是: * LineBasedFrameDecoder (换行)   LineBasedFrameDecoder是回 ...

  2. Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题

    即使关闭nagle算法,也不能解决粘包问题 https://waylau.com/netty-4-user-guide/Getting%20Started/Dealing%20with%20a%20S ...

  3. Socket粘包问题终极解决方案—Netty版(2W字)!

    上一篇我们讲了<Socket粘包问题的3种解决方案>,但没想到评论区竟然炸了.介于大家的热情讨论,以及不同的反馈意见,本文就来做一个扩展和延伸,试图找到问题的最优解,以及消息通讯的最优解决 ...

  4. Maven 解决JAR包冲突

    在JAR 冲突的情况下, 利用Eclipse方式解决JAR包冲突时比较方便简洁的,步骤如下 1. 在Eclipse 中打开pom.xml , 选择  “Dependency  Hierarchy” 2 ...

  5. python 全栈开发,Day35(TCP协议 粘包现象 和解决方案)

    一.TCP协议 粘包现象 和解决方案 黏包现象让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)执行远程命令的模块 需要用到模块subprocess sub ...

  6. TCP粘包问题的解决方案01——自定义包体

      粘包问题:应用层要发送数据,需要调用write函数将数据发送到套接口发送缓冲区.如果应用层数据大小大于SO_SNDBUF,那么,可能产生这样一种情况,应用层的数据一部分已经被发送了,还有一部分还在 ...

  7. Socket编程(4)TCP粘包问题及解决方案

    ① TCP是个流协议,它存在粘包问题 TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的.这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的.T ...

  8. UNIX网络编程——tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体 ...

  9. 基于tcp协议下粘包现象和解决方案,socketserver

    一.缓冲区 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区.write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送 ...

随机推荐

  1. 多测师讲解python函数 _zip_高级讲师肖sir

    # zip函数 #zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象,这样做的好处是节约了不少的内存.1.使用zip讲两个列表打印出来的结果是 ...

  2. 云服务器、euleros系统自动断开连接解决方案

    我这里的云服务器,网上查的修改sshd.config文件并不有效 我提供另一种方法解决这个问题: vim /etc/profile 再最底部新增 export TMOUT=6000 #6000代表60 ...

  3. 【C语言教程】“双向循环链表”学习总结和C语言代码实现!

    双向循环链表 定义 双向循环链表和它名字的表意一样,就是把双向链表的两头连接,使其成为了一个环状链表.只需要将表中最后一个节点的next指针指向头节点,头节点的prior指针指向尾节点,链表就能成环儿 ...

  4. 【CF1428D】Bouncing Boomerangs 题解

    原题链接 题意简介 毒瘤大模拟 给你一张n*n的图,在图上摆有一些物体.从每一列的底端往上扔回旋镖,每镖中一个东西,回旋镖就会向右转九十度.现在我们知道从每列i底端往上镖时撞上的物体个数ai,试构造出 ...

  5. 《Graph-Based Reasoning over Heterogeneous External Knowledge for Commonsense Question Answering》论文整理

    融合异构知识进行常识问答 论文标题 -- <Graph-Based Reasoning over Heterogeneous External Knowledge for Commonsense ...

  6. Python开发 常见异常和解决办法

    1.sqlalchemy创建外键关系报错property of that name exists on mapper SQLAlchemy是Python编程语言下的一款开源软件,提供了SQL工具包及对 ...

  7. 通过express快速搭建一个node服务

    Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台.可以理解为是运行在服务端的 JavaScript.如果你是一个前端程序员,不太擅长像PHP.Python或Ruby等 ...

  8. Java中的微信支付(1):API V3版本签名详解

    1. 前言 最近在折腾微信支付,证书还是比较烦人的,所以有必要分享一些经验,减少你在开发微信支付时的踩坑.目前微信支付的API已经发展到V3版本,采用了流行的Restful风格. 今天来分享微信支付的 ...

  9. C# NModbus RTU通信实现

    Modbus协议时应用于电子控制器上的一种通用语言.通过此协议,控制器相互之间.控制器经由网络/串口和其它设备之间可以进行通信.它已经成为了一种工业标准.有了这个通信协议,不同的厂商生成的控制设备就可 ...

  10. 监控制图OxyPlot组件的下载与安装

    1.在工具(T)-NuGet包管理器(N)-管理解决方案的NuGet程序包(N),打开组件管理界面 2.切换到浏览窗口,安装以下三个窗口组件即可 3.OxyPlot文档手册 https://oxypl ...