一、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. Centos 7.X 安装JDK1.8

    一.查看本机jdk版本并卸载原有openjdk        查看       # java -version       openjdk version "1.8.0_144"  ...

  2. Django笔记 —— 视图

    最近在学习Django,打算玩玩网页后台方面的东西,因为一直很好奇但却没怎么接触过.Django对我来说是一个全新的内容,思路想来也是全新的,或许并不能写得很明白,所以大家就凑合着看吧- 本篇笔记(其 ...

  3. Python 3基础教程12-常见的错误

    本文来介绍几种常见的错误,任何人在刚开始接触一个新的语言,即使照着代码抄写,也可能会犯错误,这里我们就介绍几种常见的错误,看看你是否遇到过. 1. NameError: name 'xxx' is n ...

  4. postman与charles的结合使用

    1.准备charles环境 Charles端口一般配置的为8888,不知道怎么配置详见charles文档 打开charles,发现访问浏览器任意页面都是失败. 在浏览器的高级设置中设置代理服务器,以火 ...

  5. XML转译字符

    &(逻辑与) & <(小于) < >(大于) > "(双引号) " '(单引号) &apos; [/size]

  6. centos 7 安装codeblocks

    CentOS7安装Code::Blocks 在CentOS7上安装Codelocks的过程. 1.安装gcc,需要c和c++两部分,默认安装下,CentOS不安装编译器的,在终端输入以下命令即可yum ...

  7. linux c编程(一)

    1 常用系统环境配置 2 使用g++编译连接,使用gdb调试 3 使用makefile组织目标文件的依赖关系 4 使用git 1 常用系统环境配置 输入法 Download setup file fo ...

  8. 聊聊、Mybatis API

    API Mybatis 到底解决了什么问题,持久化框架是什么,没出现 Mybatis 之前我们又是怎么来操作数据库的呢?对于 Java语言 来说,JDBC标准 是比较底层的了,但并非最底层的,可以说 ...

  9. php连接数据库增删改查----多条件查询

    关于查询,可以直接写在主页面上 来进行查询 首先,先建立一个表单 <form method="post" action="crud.php"> &l ...

  10. DAO设计模式的理解

    为了降低耦合性,提出了DAO封装数据库操作的设计模式. 它可以实现业务逻辑与数据库访问相分离.相对来说,数据库是比较稳定的,其中DAO组件依赖于数据库系统,提供数据库访问的接口. 一般的DAO的封装由 ...