一、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下多进程服务端客户端模型二(粘包问题与一种解决方法)的更多相关文章

  1. Linux下多进程服务端客户端模型一(单进程与多进程模型)

    本文将会简单介绍Linux下如何利用C库函数与系统调用编写一个完整的.初级可用的C-S模型. 一.基本模型: 1.1   首先服务器调用socket()函数建立一个套接字,然后bind()端口,开始l ...

  2. TCP/IP网络编程之基于TCP的服务端/客户端(二)

    回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...

  3. TCP Socket服务端客户端(二)

    本文服务端客户端封装代码转自https://blog.csdn.net/zhujunxxxxx/article/details/44258719,并作了简单的修改. 1)服务端 此类主要处理服务端相关 ...

  4. 7、Web Service-IDEA-jaxws规范下的 服务端/客户端 开发

    前提简介:这里之后即使基于IDEA进行开发的,风格与之前有些不同之处! 1.服务端的开发 1.创建新的项目 2.pom.xml 添加开发时所需要的依赖 <?xml version="1 ...

  5. APP服务端开发遇到的问题总结(后续再整理解决方法)

    IOS  AES对称加密,加密结果不同,问题解决 IOS http post请求,使用AFNetworing 框架,默认请求content-type为application/json ,所以无法使用@ ...

  6. Linux下使用Vi是方向键变乱码 退格键不能使用的解决方法

    在Linux下编辑一些文件.这就涉及到了vi这个编辑器了.在Linux下,初始使用vi的时候有点问题.就是在编辑模式下使用方向键的时候,并不会使光标移动,而是在命令行中出现[A [B [C [D之类的 ...

  7. Linux下smba服务端的搭建和客户端的使用

    解决了 windows下用root登录linuxsamba后有部分目录访问无权限的问题.应该是SELinux 设置问题. 对selinux进行修改,一般为终止这项服务,操作如下: 查看SELinux状 ...

  8. c++ 网络编程(一)TCP/UDP windows/linux 下入门级socket通信 客户端与服务端交互代码

    原文作者:aircraft 原文地址:https://www.cnblogs.com/DOMLX/p/9601511.html c++ 网络编程(一)TCP/UDP  入门级客户端与服务端交互代码 网 ...

  9. c++ 网络编程(二) linux 下多进程socket通信 多个客户端与单个服务端交互代码实现回声服务器

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9612820.html 锲子-- 预备知识优雅的关闭套接字连接: 基于TCP的半关闭 TCP中的 ...

随机推荐

  1. Kinect关于PlayerIndex和SkeletonId之间的关系。

    项目中要锁定玩家骨骼后抠图, 一时没有灵感.google 关键词: the ralationship about skeletonid and playerindex. 结论: Player Segm ...

  2. 《Cracking the Coding Interview》——第18章:难题——题目2

    2014-04-29 00:59 题目:设计一个洗牌算法,效率尽量快点,必须等概率. 解法:每次随机抽一张牌出来,最后都抽完了,也就洗好了.时间复杂度O(n^2),请看代码. 代码: // 18.2 ...

  3. 玩转Node.js(三)

    玩转Node.js(三) 上一节对于Nodejs的HTTP服务进行了较为详细的解析,而且也学会了将代码进行模块化,模块化以后每个功能都在单独的文件中,有利于代码的维护.接下来,我们要想想如何处理不同的 ...

  4. Python全栈 MySQL 数据库 (简述 、安装、基本命令)

    ParisGabriel              每天坚持手写  一天一篇  决定坚持几年 为了梦想为了信仰    开局一张图     一个月的python已经结束了  下面就是数据库了   先说M ...

  5. 孤荷凌寒自学python第十三天python代码的外部模块引用与基本赋值语句

    孤荷凌寒自学python第十三天python代码的外部模块引用与基本赋值语句 (完整学习过程屏幕记录视频地址在文末,手写笔记在文末) 从结构化编程流行以来,代码便被分块存储,称之为模块或库. 在pyt ...

  6. ironic baremetal node status

    参考: https://docs.openstack.org/ironic/latest/contributor/states.html https://docs.openstack.org/iron ...

  7. 哈希URAL 1941 - Scary Martian Word

    A - Scary Martian Word Time Limit:1000MS     Memory Limit:65536KB     64bit IO Format:%I64d & %I ...

  8. java值转递?引用传递?

    值传递是传递的是原值的副本,引用传递传递的是原值. 在Java中,如果是基本数据类型,传递的是该参数字面量值的拷贝.如果是引用数据类型,传递的是该参数所引用对象在堆中地址的拷贝. swap(int a ...

  9. intellij idea导入不了java.util.Date解决办法

    可以在Settings -> Editor -> General -> Auto Import,将Exclude中的java.util.Date删除.

  10. Vuex, api, SSR, module

    vuex https://vuex.vuejs.org/zh/guide/actions.html 单向数据流 单例模式 & 多个组件共享状态 State & 状态注入 Vue.use ...