Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)
一、Linux发送网络消息的过程
(1) 应用程序调用write()将消息发送到内核中
( 2)内核中的缓存达到了固定长度数据后,一般是SO_SNDBUF,将发送到TCP协议层
(3)IP层从TCP层收到数据,会加上自己的包头然后发送出去。一般分片的大小是MTU(含IP包头),而IPV4下IP的包头长度为40,而IPV6下为60,因此,TCP中分片后,有效的数据长度为MSS = MTU - 40 或 MSS = MTU -60
(4)最终经过其他层的包装,发送到公网上,跑来跑去,这时候,你的数据可能几段连为一条,一条可能分为几段。

二、粘包问题
上一篇文章中,我们用write()系统调用来读取数据,但是这个调用需要指定长度,例如上文中的1024,那么问题来了:
(1)报文有效数据长1025怎么办 ? 对方发“Hi,I like you!” 你期望收到“Hi, I like ”吗
(2)报文有效数据长度300怎么办? 对方发“Hi, I like you!” "You are befutiful" 你期望收到“Hi, I like you!You are”么? 你不想知道 you are 什么,还有,明明对方发送了两条消息,而你。。。收到了一条半,还当作了一条。
三、解决
3.1主要有两种解决方案,分别为
(1)认为的加边界 例如以\R\N为界限,FTP协议就是用的这种方法。
(2)建立一个数据结构,如下:
struct packet
{
int len;
char buff[];
};
发送前,将packet.len设置好,然后将该数据结构的一个实例发送过去,读的时候先读取int长度即4个字节的数据,获得buff的有效长度,然后循环读,直到读够len字节的数据为止。
本文主要介绍第二种设定数据结构的方案。该方案的一个小缺点是,单次写不会超过buff[1024]的大小限制。。。
3.2 readn函数:
ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}
readn
3.3 writen函数;
ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}
writen
3.4利用这两个函数,即可完成读写。下文将介绍利用这两个函数完成的一个P2P程序,服务端与客户端互相发送用户输入的数据:程序的架构如下:

3.4.1 服务端:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h> #include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h> #define ERR_EXIT(m) \
do { \
perror(m);\
exit(EXIT_FAILURE);\
}while() struct packet
{
int len;
char buff[];
}; ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}
ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}
void handle(int sig)
{
printf("recv sig = %d\n", sig);
exit();
}
int main(void)
{
signal(SIGUSR1,handle); int sockfd;
// 创建一个Socket
sockfd = socket(AF_INET,SOCK_STREAM,);
if(sockfd == -){
perror("error");
exit();
} ///////////////////////////////////////////////////////////
// struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
struct sockaddr_in sockdata;
sockdata.sin_family = AF_INET;
sockdata.sin_port = htons();
sockdata.sin_addr.s_addr = inet_addr("192.168.59.128"); int optval = ;
if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -)
{
perror("error");
exit();
}
if(bind(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) < ){
perror("error");
exit();
} ////////////////////////////////////////////////////////////
if(listen(sockfd,SOMAXCONN) == -){ //变成被动侦听套接字。
perror("error");
exit();
} //////////////////////////////////////////////////////////
struct sockaddr_in peeradr;
socklen_t peerlen = sizeof(peeradr); // 得有初始值 /////////////////////////////////////////////////////////
int conn = ;
conn = accept(sockfd,(struct sockaddr *)&peeradr,&peerlen);
if(conn == -){
perror("error");
exit();
} printf("收到的IP %s\n 客户端端口是:%d\n,conn == %d\n",inet_ntoa(peeradr.sin_addr),ntohs(peeradr.sin_port),conn); pid_t twopid;
twopid = fork(); if(twopid == -){
perror("error");
exit();
}
if(twopid > ){ // father , 接受数据
struct packet recvBuff;
memset(&recvBuff,,sizeof(recvBuff));
int ret = ;
int rn;
while(){
ret = readn(conn,&recvBuff,); // 先获得长度
if(ret == -){
ERR_EXIT("READ");
}
if(ret < ){
printf("client close\n");
break;
}
rn = ntohl(recvBuff.len);
ret = readn(conn,recvBuff.buff,rn);
if(ret == -){
ERR_EXIT("READ");
}
if(ret < rn){
printf("client close\n");
break;
}
fputs(recvBuff.buff,stdout);
memset(&recvBuff,,sizeof(recvBuff));
}
printf("client closed"); // may this create a guer process
// send signal to child
kill(twopid, SIGUSR1);
close(conn);
close(sockfd);
sleep();
exit();
}
if(twopid == ){ // child send data
close(sockfd);
int n;
struct packet sendBuff;
memset(&sendBuff,,sizeof(sendBuff));
while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){
n = strlen(sendBuff.buff);
sendBuff.len = htonl(n);
writen(conn,&sendBuff,+n);
memset(&sendBuff,,sizeof(sendBuff));
}
exit();
}
return ;
}
server.c
3.4.2 客户端:
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h> #include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h> #include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#define ERR_EXIT(m) \
do { \
perror(m);\
exit(EXIT_FAILURE);\
}while() struct packet
{
int len;
char buff[];
}; ssize_t readn(int sock, void *recv, size_t len)
{
size_t nleft = len;
ssize_t nread;
char *bufp = (char*)recv; // 辅助指针变量,记录位置的。
while(nleft > ){
if((nread = read(sock,bufp,nleft)) < ){ //read error 读len,当然可能被中断读不够len,所以继续
if(errno == EINTR){ // 被信号中断到
continue;
}
return -;
}
else if(nread == ){ // 若对方已关闭,返回已读字数。
return len - nleft;
}
bufp += nread; // mov point
nleft -= nread;
}
return len;
}
ssize_t writen(int sock,const void *buf, size_t len)
{
size_t nleft = len;
ssize_t nwrite;
char *bufp = (char*)buf; while(nleft > ){
if((nwrite = write(sock,bufp,nleft)) < ){
if(errno == EINTR){ // 信号中断
continue;
}
return -;
}
else if(nwrite == ){ // write返回值是0,代表对方套接字关闭,再写会失败,然后返回。
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return len;
}
int main(void)
{
int sockfd;
// 创建一个Socket
sockfd = socket(AF_INET,SOCK_STREAM,);
if(sockfd == -){
perror("error");
exit();
} ///////////////////////////////////////////////////////////
// struct sockaddr addr; // 这是一个通用结构,一般是用具体到,然后转型
struct sockaddr_in sockdata;
sockdata.sin_family = AF_INET;
sockdata.sin_port = htons();
sockdata.sin_addr.s_addr = inet_addr("192.168.59.128");
if(connect(sockfd,(struct sockaddr *)&sockdata,sizeof(sockdata)) == -){
perror("error");
exit();
}
pid_t pid = ;
pid = fork();
if(pid == -){ perror("error");
exit();
}
if(pid > ){ // father // ccept data from keyboad
struct packet sendBuff;
memset(&sendBuff,,sizeof(sendBuff));
int n;
while(fgets(sendBuff.buff,sizeof(sendBuff.buff),stdin) != NULL){ n = strlen(sendBuff.buff);
// 设置发送消息到长度。
sendBuff.len = htonl(n);
// 将结构体实例写入。
writen(sockfd,&sendBuff,+n); // 清零
memset(&sendBuff,,sizeof(sendBuff));
} }
if(pid == ){ // child recv data
struct packet recvBuff;
memset(&recvBuff,,sizeof(recvBuff));
// 从服
int ret;
int rn;
while(){
// 首先获得要读取到长度,前4个字节
ret = readn(sockfd,&recvBuff.len,);
if(ret == -){
ERR_EXIT("READ");
}
if(ret < ){
printf("server close\n");
break;
} // 读取4个字节开始到数据。
rn = ntohl(recvBuff.len);
ret = readn(sockfd,recvBuff.buff,rn);
if(ret == -){
ERR_EXIT("read error");
}
if(ret < rn ){
printf("server close\n");
break;
}
// put it to screen
fputs(recvBuff.buff,stdout);
// 清零
memset(&recvBuff,,sizeof(recvBuff));
} } close(sockfd);
return ;
}
client.c
后记:
由于上文中获得len的大小的方式是 测试buff[1024]中有效数据的长度的,所以实际上len每一个不会超过1024。
但是,当fgets函数接受的一行长度大于1023的时候,它会将剩下的(1023以后的)字符串作为下一次的输入。然后发送端会发送两个Packet实例。
而接收端接收到的两个pocket都有正确的长度,所以可以安全的接受,但是不幸的是,会将一条报文分成多条。。。
该程序是单进程的,读者可以自行改成多进程的。
Linux下多进程服务端客户端模型二(粘包问题与一种解决方法)的更多相关文章
- Linux下多进程服务端客户端模型一(单进程与多进程模型)
本文将会简单介绍Linux下如何利用C库函数与系统调用编写一个完整的.初级可用的C-S模型. 一.基本模型: 1.1 首先服务器调用socket()函数建立一个套接字,然后bind()端口,开始l ...
- TCP/IP网络编程之基于TCP的服务端/客户端(二)
回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...
- TCP Socket服务端客户端(二)
本文服务端客户端封装代码转自https://blog.csdn.net/zhujunxxxxx/article/details/44258719,并作了简单的修改. 1)服务端 此类主要处理服务端相关 ...
- 7、Web Service-IDEA-jaxws规范下的 服务端/客户端 开发
前提简介:这里之后即使基于IDEA进行开发的,风格与之前有些不同之处! 1.服务端的开发 1.创建新的项目 2.pom.xml 添加开发时所需要的依赖 <?xml version="1 ...
- APP服务端开发遇到的问题总结(后续再整理解决方法)
IOS AES对称加密,加密结果不同,问题解决 IOS http post请求,使用AFNetworing 框架,默认请求content-type为application/json ,所以无法使用@ ...
- Linux下使用Vi是方向键变乱码 退格键不能使用的解决方法
在Linux下编辑一些文件.这就涉及到了vi这个编辑器了.在Linux下,初始使用vi的时候有点问题.就是在编辑模式下使用方向键的时候,并不会使光标移动,而是在命令行中出现[A [B [C [D之类的 ...
- Linux下smba服务端的搭建和客户端的使用
解决了 windows下用root登录linuxsamba后有部分目录访问无权限的问题.应该是SELinux 设置问题. 对selinux进行修改,一般为终止这项服务,操作如下: 查看SELinux状 ...
- c++ 网络编程(一)TCP/UDP windows/linux 下入门级socket通信 客户端与服务端交互代码
原文作者:aircraft 原文地址:https://www.cnblogs.com/DOMLX/p/9601511.html c++ 网络编程(一)TCP/UDP 入门级客户端与服务端交互代码 网 ...
- c++ 网络编程(二) linux 下多进程socket通信 多个客户端与单个服务端交互代码实现回声服务器
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9612820.html 锲子-- 预备知识优雅的关闭套接字连接: 基于TCP的半关闭 TCP中的 ...
随机推荐
- Kinect关于PlayerIndex和SkeletonId之间的关系。
项目中要锁定玩家骨骼后抠图, 一时没有灵感.google 关键词: the ralationship about skeletonid and playerindex. 结论: Player Segm ...
- 《Cracking the Coding Interview》——第18章:难题——题目2
2014-04-29 00:59 题目:设计一个洗牌算法,效率尽量快点,必须等概率. 解法:每次随机抽一张牌出来,最后都抽完了,也就洗好了.时间复杂度O(n^2),请看代码. 代码: // 18.2 ...
- 玩转Node.js(三)
玩转Node.js(三) 上一节对于Nodejs的HTTP服务进行了较为详细的解析,而且也学会了将代码进行模块化,模块化以后每个功能都在单独的文件中,有利于代码的维护.接下来,我们要想想如何处理不同的 ...
- Python全栈 MySQL 数据库 (简述 、安装、基本命令)
ParisGabriel 每天坚持手写 一天一篇 决定坚持几年 为了梦想为了信仰 开局一张图 一个月的python已经结束了 下面就是数据库了 先说M ...
- 孤荷凌寒自学python第十三天python代码的外部模块引用与基本赋值语句
孤荷凌寒自学python第十三天python代码的外部模块引用与基本赋值语句 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) 从结构化编程流行以来,代码便被分块存储,称之为模块或库. 在pyt ...
- ironic baremetal node status
参考: https://docs.openstack.org/ironic/latest/contributor/states.html https://docs.openstack.org/iron ...
- 哈希URAL 1941 - Scary Martian Word
A - Scary Martian Word Time Limit:1000MS Memory Limit:65536KB 64bit IO Format:%I64d & %I ...
- java值转递?引用传递?
值传递是传递的是原值的副本,引用传递传递的是原值. 在Java中,如果是基本数据类型,传递的是该参数字面量值的拷贝.如果是引用数据类型,传递的是该参数所引用对象在堆中地址的拷贝. swap(int a ...
- intellij idea导入不了java.util.Date解决办法
可以在Settings -> Editor -> General -> Auto Import,将Exclude中的java.util.Date删除.
- Vuex, api, SSR, module
vuex https://vuex.vuejs.org/zh/guide/actions.html 单向数据流 单例模式 & 多个组件共享状态 State & 状态注入 Vue.use ...