主要内容:

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. java基础知识总结(续写)

    1.两个容易搞混的C盘文件夹 文件名 描述 Progrm Files 默认存储的64位软件 Progrm Files(x86) 默认存储32位软件 2.常用基础DOS命令(Windows+R打开命令) ...

  2. MeteoInfoLab脚本示例:多坐标系

    绘图的时候首先要有坐标系(Axes),可以用axes命令创建,如果没有创建在绘图时会自动创建一个.参数里的position是用来置顶坐标系的图形(figure)中的位置的,通过位置置顶,可以将多个坐标 ...

  3. 如何从0到1的构建一款Java数据生成器-第一章

    前提 某天晚上老夫在神游时,想起白天公司同事说起的问题,这老表抱怨使用mysql生成大批的随机测试数据太过麻烦,问大家有没有好的工具推荐,老夫对这种事情当然不关心,毕竟我也不知道. 秉承着不懂就要问, ...

  4. day60 Pyhton 框架Django 03

    day61 内容回顾 1.安装 1. 命令行: pip install django==1.11.18 pip install django==1.11.18 -i 源 2. pycharm sett ...

  5. 写给前端同学的C++入门教程(一):概述和环境搭建

    说明:本人是前端er,因为最近对 UE4(一个游戏开发引擎)产生了兴趣,而这个引擎源开发游戏时需要用到 C++ ,所以就开始入坑 C++ 了.现将自己学习 C++ 的笔记整理并分享出来,以便一些想入门 ...

  6. 【暑假集训】HZOI2019 Luogu P1006 传纸条 二三四维解法

    写三次丢失两次,我谔谔,以后再不在博客园先保存我就去死 题目内容 洛谷链接 小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题.一次素质拓展活动中,班上同学被安排坐成一个\(m\)行.\(n\ ...

  7. widows安装ffmpeg

    首先下载ffmpeg的windows版本https://ffmpeg.zeranoe.com/builds/ 解压到d盘 win+r cmd 说明成功了

  8. ansible使用shell模块在受控机上执行命令(ansible2.9.5)

    一,ansible的shell模块和command模块的区别? shell模块:在远程主机上执行主控端发出的shell/python脚本 command模块:不能调用shell指令,没有bash的环境 ...

  9. TCP/IP的十个问题

    一.TCP/IP模型 TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的 ...

  10. linux修改环境变量后无法登录

    在登陆界面按Ctrl+Alt+F1(F1~F6), 进入 tty 后登陆账号. 执行以下命令: /usr/bin/sudo /usr/bin/vi /etc/environment 将PATH的值复原 ...