一、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. ADMX Migrator

    实用工具特别推荐ADMX MigratorLance Whitney 下载这篇文章的代码: ADMX Migrator (2765KB) 对于那些 使用组策略的人而言,他们自然非常熟悉如何使用管理模板 ...

  2. global js库

    var GLOBAL = {}; GLOBAL.namespace = function(str) { var arr = str.split("."), o = GLOBAL,i ...

  3. 【java下午茶系列】java三重奏之封装

      java中的封装.继承.多态可谓是踏入这一行业的必经之槛,诸多新人在不明就里的情况下将其各种概念背的是滚瓜烂熟.即便是工作多年之后,也不见得能说出个所以然,或许冥冥之中已经写过无数封装的代码,只是 ...

  4. 不作伪分享者决定完整分享我自学Python3的全部过程细节

    不作伪分享者决定完整分享我自学Python3的全部过程细节   我不要作伪分享者 十六年前我第一次见到了电脑,并深深地爱上了它: 十二年前我第一次连上了网络,并紧紧地被它爱上. 十年前的网络是田园美景 ...

  5. Where can I find the IPA logs

    Retrieving the IPA logs will differ depending on which base image was used. Operating system that do ...

  6. leetcode 208. 实现 Trie (前缀树)

    实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作. 示例: Trie trie = new Trie(); trie.insert(" ...

  7. win10&hyper上装Ubuntu出现没有找到dev fd0, sector 0 错误

    win10 hyper装 ubuntu blk_update_request:I/O error,dev sr0,sector0 错误 配置好安装重启后出现 blk_update_request: I ...

  8. 201621123034 《Java程序设计》第8周学习总结

    作业08-集合 1. 本周学习总结 以你喜欢的方式(思维导图或其他)归纳总结集合相关内容. 2. 书面作业 1. ArrayList代码分析 1.1 解释ArrayList的contains源代码 答 ...

  9. 【Luogu】P4284概率充电器(概率树形DP)

    题目链接 这题好神啊…… 设f[i]为i没电的概率,初始化$f[i]=1-q[i]$ 之后x的电有三个来源: 1.x自己有电 2.x的儿子给它传来了电 3.x的父亲给它传来了电 对于2和3操作分别做一 ...

  10. HITOJ 2739 The Chinese Postman Problem(欧拉回路+最小费用流)

    The Chinese Postman Problem My Tags   (Edit)   Source : bin3   Time limit : 1 sec   Memory limit : 6 ...