linux网络编程之socket编程(四)
经过两周的等待,终于可以回归我正常的学习之旅了,表哥来北京了在我这暂住,晚上回家了基本在和他聊天,周末带他在北京城到处乱转,几乎剥夺了我自由学习的时间了,不过,亲人之情还是很难得的,工作学习并不是生活的唯一,现在已经习惯每周至少写一篇博文的生活了,如果一周不写会觉得缺少什么似的,好了,话不多说,继续学习linux网络编程socket相关的知识:
流协议与粘包:
关于什么是粘包可能有些抽象,先得有一些理论基础:我们知道TCP是一个基于字节流的传输服务,这意味着TCP所传输的数据之间是无边界的,像流水一样,是无法区分边界的;而UDP是基于消息的传输服务,它传输的是数据报文,是有边界的。
而对于数据之间有无边界,反映在对方接收程序的时候,是不一样的:对于TCP字节流来说,对等方在接收数据的时候,不能够保证一次读操作,能够返回多少个字节,是一个消息,还是二个消息,这些都是不确定的;而对于UDP消息服务来说,它能够保证对等方一次读操作返回的是一条消息。
由于TCP的无边界性,就会产生粘包问题,那粘包问题具体体现是怎样的呢?下面用图来进行阐述:
假设主机A(Host A)要向主机B(Host B)发送两个数据包:M1,M2
而对于对待接收方主机B来说,可能会有以下几种情况:
①
也就是第一次读操作刚好返回第一条消息(M1)的全部,接下来第二次读操作返回第二条消息(M2)的全部,所以这就没有粘包问题。
②
一次读操作就返回了M1,M2的所有,这样M1和M2就粘在一起了,这就能比较直观的体会到粘包的表现了。
③
一次读操作返回了M1的全部,并且还有M2的一部分(m2_1);第二次读操作返回了M2的另外一部分(M2_2)。
④
一次读操作返回了M1的一部分(M1_1);第二次读操作返回了M1的另外一部分(M1_2),并且还有M2的全部。
当然除了上面四种情况,可能还存在其它组合,因为主机B一次能接收的字节数是不确定的。
下面来探讨下产生的原因。
粘包产生的原因
① 应用程要将自己缓冲区中的数据发送出去,首先要调用一个write方法,将应用程序的缓冲区的数据拷贝到套接口发送缓冲区(SO_SNDBUF),而该缓冲区有一个SO_SNDBUF大小的限制,如果应用缓冲区一条消息的大小超过了SO_SNDBUF的大小,那这时候就有可能产生粘包问题,因为消息被分隔了,一部分已经发送给发送缓冲区,且对方已经接收到了,另外一部分才放到了发送缓冲区,这样对方就延迟接收了消息的后一部分。这就导致了粘包问题的出现。
②TCP传输的段有最大段(MSS)的限制,所以也会对应用发送的消息进行分割而产生粘包问题。
③链路层它所传输的数据有一个最大传输单元(MTU)的限制,如果我们所发送的数据包超过了最大传输单元,会在IP层进行分组,这也可能导致消息的分割,所以也有可能出现粘包问题。
当然还有其它原因,如TCP的流量控制、拥塞控制、TCP的延迟发送机制,对于上面说的理论理解起来比较抽象,只要记住一条:TCP会产生粘包问题既可。
粘包解决方案:
怎么才能解决粘包问题呢?
既然TCP协议没有在传输层没有维护消息与消息之间的边界,所以:
我们所要发送的消息是一个定长包,那么对等方在接收的时候已定长的方式来进行接收,就能确保消息与消息之间的边界。
这种方式有个问题,就是如果消息本身就带这些字符的话,就无法就无法区分消息的边界了,这时就需要用到转义字符了。
其中包头是定长的,如4个字节。
这些解决方案有一个很重要的问题,就是定长包的接收,我们之前说了,TCP是一个流协议,它不能保证对方一次接收接收到了多少个字节,那我们就需要封装一个函数:接收确定字节数的读操作。
下面来封装两个函数,如下:
readn、writen
接收确切数目的读操作
我们还是继续完善之前的回射服务客户端的程序,关于这个程序可以回顾一下http://www.cnblogs.com/webor2006/p/3932917.html
先贴一下代码:
echosrv.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() void do_service(int conn)
{
char recvbuf[];
while ()
{
memset(recvbuf, , sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
if (ret == )
{
printf("client close\n");
break;
}
else if (ret == -)
ERR_EXIT("read");
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
} int main(void)
{
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
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);*/ int on = ;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < )
ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < )
ERR_EXIT("listen"); struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn; pid_t pid;
while ()
{
if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < )
ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pid = fork();
if (pid == -)
ERR_EXIT("fork");
if (pid == )
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
}
else
close(conn);
} return ;
}
echocli.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("connect"); char sendbuf[] = {};
char recvbuf[] ={};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout);
memset(sendbuf, , sizeof(sendbuf));
memset(recvbuf, , sizeof(recvbuf));
} close(sock); return ;
}
对于这个函数的封装,还是参考这个原形来设计,参数保持一样:
这样,最后用我们写的函数来替换这个系统调用既可,下面则正式开始封装此函数:
ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号的整数,ssize_t是有符号的整数
{
size_t nleft = count;//剩余的字节数
ssize_t nread;//已接收的字节数
char *bufp = (char*)buf; while (nleft > )
{//由于不能保证一次读操作能够返回字节数是多少,所以需要进行循环来接收
if ((nread = read(fd, bufp, nleft)) < )
{
if (errno == EINTR)//被信号中断了,则继续执行,因为不是出错
continue;
return -;//表示读取失败了
}
else if (nread == )//对等方关闭了
return count - nleft;//返回已经读取的字节数 bufp += nread;
nleft -= nread;
} return count;
}
【说明】:关于这个函数的编写,可以好好理解下,目的就是用我们自己封装的方法来代替系统的读方法。
发送确切数目的写操作
关于这个方法,跟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 > )
{
if ((nwritten = write(fd, bufp, nleft)) < )
{
if (errno == EINTR)
continue;
return -;
}
else if (nwritten == )//如果是这种情况,则表示什么都没发生,继续还得执行
continue; bufp += nwritten;
nleft -= nwritten;
} return count;
}
接下来,用我们自己封装的函数来代码系统函数,先只修改客户端程序,一步步来引导其这样做的原因。
echosrv.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() 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;
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;
} void do_service(int conn)
{
char recvbuf[];
while ()
{
memset(recvbuf, , sizeof(recvbuf));
int ret = readn(conn, recvbuf, sizeof(recvbuf));//将其替换成自己封装的方法
if (ret == )
{
printf("client close\n");
break;
}
else if (ret == -)
ERR_EXIT("read");
fputs(recvbuf, stdout);
writen(conn, recvbuf, ret);
}
} int main(void)
{
int listenfd;
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
/* if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
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);*/ int on = ;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < )
ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("bind");
if (listen(listenfd, SOMAXCONN) < )
ERR_EXIT("listen"); struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn; pid_t pid;
while ()
{
if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < )
ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); pid = fork();
if (pid == -)
ERR_EXIT("fork");
if (pid == )
{
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
}
else
close(conn);
} return ;
}
这时客户端程序还保持原样,这时编译,那会有什么效果呢:
发现,这时客户端发送的数据服务端没有办法接收了,这是为什么呢?
如果对方发送数据不足1024个字节时,那就会一直循环,查看其readn函数:
这时,解决方案,第一种就是发送定长包:
所以,这时将客户端的write替换成writen,如下:
echocli.c:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h> #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while() 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;
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;
} int main(void)
{
int sock;
if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < )
ERR_EXIT("socket"); struct sockaddr_in servaddr;
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
ERR_EXIT("connect"); char sendbuf[] = {};
char recvbuf[] ={};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
writen(sock, sendbuf, sizeof(sendbuf));
readn(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout);
memset(sendbuf, , sizeof(sendbuf));
memset(recvbuf, , sizeof(recvbuf));
} close(sock); return ;
}
这时再运行,看问题是否解决:
这时就解决了之前的问题,但是有一个问题,每次发送都是1024定长的字节,如果只发送几个字节的内容也会占用这么多字节,这就会增加网络的负担,那怎么解决这个问题呢?
这时候需要自己定义一个协议,可以定义这样一个包的结构:
这时,在发送数据时,就得进行相应的修改,如下:
这时,服务端接收数据时,也需要进行修改:
当服务端接收完之后,接着回显给客户端,代码修改如下:
这时客户端接收也同理,也是先读取包长度,然后再接收包数据,修改如下:
至此,就已经将解决定长字长的问题的代码写完了,下面来编译运行一下:
至此,这样就很好的解决了粘包问题,在局域网中是不可能出现粘包问题的,但是如果将程序放到广域网,如果不处理粘包问题会存在很大问题的。好了,今天就学到这,下节见~
linux网络编程之socket编程(四)的更多相关文章
- linux网络编程之socket编程(一)
今天开始,继续来学习linux编程,这次主要是研究下linux下的网络编程,而网络编程中最基本的需从socket编程开始,下面正式开始学习: 什么是socket: 在学习套接口之前,先要回顾一下Tcp ...
- linux网络编程之socket编程(六)
经过一个国庆长假,又有一段时间没有写博文了,今天继续对linux网络编程进行学习,如今的北京又全面进入雾霾天气了,让我突然想到了一句名句:“真爱生活,珍惜生命”,好了,言归正传. 回顾一下我们之间实现 ...
- linux网络编程之socket编程(八)
学习socket编程继续,今天要学习的内容如下: 先来简单介绍一下这五种模型分别是哪些,偏理论,有个大致的印象就成,做个对比,因为最终只会研究一个I/O模型,也是经常会用到的, 阻塞I/O: 先用一个 ...
- linux网络编程之socket编程(二)
今天继续对socket编程进行研究,这里会真正开如用socket写一个小例子,进入正题: TCP客户/服务器模型: 关于这个模型的流程这里就不多说了,比较容易理解,下面则利用这种模型来编写一个实际 ...
- linux网络编程之socket编程(七)
今天继续学习socket编程,北京在持续几天的雾霾天之后久违的太阳终于出来了,心情也特别特别的好,于是乎,在这美好的夜晚,该干点啥事吧,那当然就是继续坚持我的程序学习喽,闲话不多说,进入正题: 通过这 ...
- linux网络编程之socket编程(十六)
继续学习socket编程,今天的内容会有些难以理解,一步步来分解,也就不难了,正入正题: 实际上sockpair有点像之前linux系统编程中学习的pipe匿名管道,匿名管道它是半双工的,只能用于亲缘 ...
- linux网络编程之socket编程(十五)
今天继续学习socket编程,这次主要是学习UNIX域协议相关的知识,下面开始: [有个大概的认识,它是来干嘛的] ①.UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍. ...
- linux网络编程之socket编程(三)
今天继续对socket编程进行学习,在学习之前,需要回顾一下上一篇中编写的回射客户/服务器程序(http://www.cnblogs.com/webor2006/p/3923254.html),因为今 ...
- linux网络编程之socket编程(十二)
今天继续学习socket编程,期待的APEC会议终于在京召开了,听说昨晚鸟巢那灯火通明,遍地礼花,有点08年奥运会的架势,有种冲动想去瞅见一下习大大的真容,"伟大的祖国,我爱你~~~&quo ...
随机推荐
- [LeetCode] 168. Excel Sheet Column Title 求Excel表列名称
Given a positive integer, return its corresponding column title as appear in an Excel sheet. For exa ...
- QualityCenter(QC)—测试管理工具
简介 Quality Center是一个基于Web的测试管理工具,可以组织和管理应用程序测试流程的所有阶段,包括制定测试需求.计划测试.执行测试和跟踪缺陷.此外,通过Quality Center还可以 ...
- 面试必备:Java 原子操作的实现原理[精品长文]
本文整理自<Java并发编程的艺术>第二章 作者:方腾飞 魏鹏 程晓明 原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被 ...
- windows 安装node.js
安装node.js 这里我们是直接下载的编译后的zip包 地址:https://nodejs.org/en/download/ 点击下载相应的zip版本 这里我把zip包最里面一层的东西,所有内容放在 ...
- JAVA代码:生成一个集合,自定义大小,100以内的随机整数
JAVA代码:生成一个集合,自定义大小,100以内的随机整数 方法一:(Random类) package com.dawa.test; import java.util.ArrayList; impo ...
- 第07组 Beta冲刺(2/4)
队名:秃头小队 组长博客 作业博客 组长徐俊杰 过去两天完成的任务:学习了很多东西 Github签入记录 接下来的计划:继续学习 还剩下哪些任务:后端部分 燃尽图 遇到的困难:自己太菜了 收获和疑问: ...
- c++中的不是数的数nan
matlab中经常碰到nan,inf等特殊“数”,而在C++中也有相应的表示:例如std::numeric_limits <float>::quiet_NaN ();可以得到浮点型的nan ...
- Java开发笔记(一百三十二)Swing的表格
前面介绍了程序界面上一些简单控件的组合排列,它们用来表达相互之间联系较弱的信息倒还凑合,要是用来表达关联性较强的聚合信息就力不从心了.倘若只是简单信息的罗列,例如商品名称列表.新闻标题列表.学生姓名列 ...
- Word 中直引号和弯引号的相互替换
直引号替换成弯引号 弯引号替换成直引号 未完 ...... 点击访问原文(进入后根据右侧标签,快速定位到本文)
- git学习笔记 ---版本退回
我们已经成功地添加并提交了一个readme.txt文件,现在,是时候继续工作了,于是,我们继续修改readme.txt文件,改成如下内容: Git is a distributed version c ...