linux网络编程之socket编程(四)

经过两周的等待,终于可以回归我正常的学习之旅了,表哥来北京了在我这暂住,晚上回家了基本在和他聊天,周末带他在北京城到处乱转,几乎剥夺了我自由学习的时间了,不过,亲人之情还是很难得的,工作学习并不是生活的唯一,现在已经习惯每周至少写一篇博文的生活了,如果一周不写会觉得缺少什么似的,好了,话不多说,继续学习linux网络编程socket相关的知识:

流协议与粘包:

关于什么是粘包可能有些抽象,先得有一些理论基础:我们知道TCP是一个基于字节流的传输服务,这意味着TCP所传输的数据之间是无边界的,像流水一样,是无法区分边界的;而UDP是基于消息的传输服务,它传输的是数据报文,是有边界的。

而对于数据之间有无边界,反映在对方接收程序的时候,是不一样的:对于TCP字节流来说,对等方在接收数据的时候,不能够保证一次读操作,能够返回多少个字节,是一个消息,还是二个消息,这些都是不确定的;而对于UDP消息服务来说,它能够保证对等方一次读操作返回的是一条消息

由于TCP的无边界性,就会产生粘包问题,那粘包问题具体体现是怎样的呢?下面用图来进行阐述:

linux网络编程之socket编程(四)

假设主机A(Host A)要向主机B(Host B)发送两个数据包:M1,M2

而对于对待接收方主机B来说,可能会有以下几种情况:

linux网络编程之socket编程(四)

 也就是第一次读操作刚好返回第一条消息(M1)的全部,接下来第二次读操作返回第二条消息(M2)的全部,所以这就没有粘包问题。

 linux网络编程之socket编程(四)

 一次读操作就返回了M1,M2的所有,这样M1和M2就粘在一起了,这就能比较直观的体会到粘包的表现了。

 linux网络编程之socket编程(四)

 一次读操作返回了M1的全部,并且还有M2的一部分(m2_1);第二次读操作返回了M2的另外一部分(M2_2)。

 linux网络编程之socket编程(四)

一次读操作返回了M1的一部分(M1_1);第二次读操作返回了M1的另外一部分(M1_2),并且还有M2的全部。

当然除了上面四种情况,可能还存在其它组合,因为主机B一次能接收的字节数是不确定的。

下面来探讨下产生的原因。

粘包产生的原因

linux网络编程之socket编程(四)

① 应用程要将自己缓冲区中的数据发送出去,首先要调用一个write方法,将应用程序的缓冲区的数据拷贝到套接口发送缓冲区(SO_SNDBUF),而该缓冲区有一个SO_SNDBUF大小的限制,如果应用缓冲区一条消息的大小超过了SO_SNDBUF的大小,那这时候就有可能产生粘包问题,因为消息被分隔了,一部分已经发送给发送缓冲区,且对方已经接收到了,另外一部分才放到了发送缓冲区,这样对方就延迟接收了消息的后一部分。这就导致了粘包问题的出现。

②TCP传输的段有最大段(MSS)的限制,所以也会对应用发送的消息进行分割而产生粘包问题。

③链路层它所传输的数据有一个最大传输单元(MTU)的限制,如果我们所发送的数据包超过了最大传输单元,会在IP层进行分组,这也可能导致消息的分割,所以也有可能出现粘包问题。

当然还有其它原因,如TCP的流量控制、拥塞控制、TCP的延迟发送机制,对于上面说的理论理解起来比较抽象,只要记住一条:TCP会产生粘包问题既可。

 

粘包解决方案:

怎么才能解决粘包问题呢?

既然TCP协议没有在传输层没有维护消息与消息之间的边界,所以:

linux网络编程之socket编程(四)

linux网络编程之socket编程(四)

  我们所要发送的消息是一个定长包,那么对等方在接收的时候已定长的方式来进行接收,就能确保消息与消息之间的边界。

linux网络编程之socket编程(四)
 

这种方式有个问题,就是如果消息本身就带这些字符的话,就无法就无法区分消息的边界了,这时就需要用到转义字符了。

linux网络编程之socket编程(四)

其中包头是定长的,如4个字节。

linux网络编程之socket编程(四)

这些解决方案有一个很重要的问题,就是定长包的接收,我们之前说了,TCP是一个流协议,它不能保证对方一次接收接收到了多少个字节,那我们就需要封装一个函数:接收确定字节数的读操作

下面来封装两个函数,如下:

readn、writen

linux网络编程之socket编程(四)

 接收确切数目的读操作

我们还是继续完善之前的回射服务客户端的程序,关于这个程序可以回顾一下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(0)

void do_service(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                write(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    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 = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}

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(0)

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}

 

对于这个函数的封装,还是参考这个原形来设计,参数保持一样:

linux网络编程之socket编程(四)

这样,最后用我们写的函数来替换这个系统调用既可,下面则正式开始封装此函数:

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 > 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;
}

【说明】:关于这个函数的编写,可以好好理解下,目的就是用我们自己封装的方法来代替系统的读方法。 

linux网络编程之socket编程(四)

发送确切数目的写操作

关于这个方法,跟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 > 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;
}

接下来,用我们自己封装的函数来代码系统函数,先只修改客户端程序,一步步来引导其这样做的原因。

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(0)

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[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readn(conn, recvbuf, sizeof(recvbuf));//将其替换成自己封装的方法
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    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 = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}

这时客户端程序还保持原样,这时编译,那会有什么效果呢:

linux网络编程之socket编程(四)

发现,这时客户端发送的数据服务端没有办法接收了,这是为什么呢?

linux网络编程之socket编程(四)

如果对方发送数据不足1024个字节时,那就会一直循环,查看其readn函数:

linux网络编程之socket编程(四)

这时,解决方案,第一种就是发送定长包:

linux网络编程之socket编程(四)

所以,这时将客户端的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(0)

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)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sock, sendbuf, sizeof(sendbuf));
        readn(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}

这时再运行,看问题是否解决:

linux网络编程之socket编程(四)

这时就解决了之前的问题,但是有一个问题,每次发送都是1024定长的字节,如果只发送几个字节的内容也会占用这么多字节,这就会增加网络的负担,那怎么解决这个问题呢?

这时候需要自己定义一个协议,可以定义这样一个包的结构:

linux网络编程之socket编程(四)

这时,在发送数据时,就得进行相应的修改,如下:

linux网络编程之socket编程(四)

这时,服务端接收数据时,也需要进行修改:

linux网络编程之socket编程(四)

当服务端接收完之后,接着回显给客户端,代码修改如下:

linux网络编程之socket编程(四)

这时客户端接收也同理,也是先读取包长度,然后再接收包数据,修改如下:

linux网络编程之socket编程(四)

至此,就已经将解决定长字长的问题的代码写完了,下面来编译运行一下:

linux网络编程之socket编程(四)

至此,这样就很好的解决了粘包问题,在局域网中是不可能出现粘包问题的,但是如果将程序放到广域网,如果不处理粘包问题会存在很大问题的。好了,今天就学到这,下节见~

 

更多相关文章
  • 7月的赌城拉斯维加斯,一家豪华五星级酒店的千人会议厅,来自世界各地的电脑"黑客"高手齐聚一堂. 其名虽黑,不过"黑客"不全是电脑世界的黑道人物,其中也不乏白道高手. 黑道是指那些做"地下工作"的网上高手,他们能攻破最坚固的网络,制造病毒,让 ...
  • 解决SQL Server 2008提示评估期已过 第一步:进入SQL2008配置工具中的安装中心 第二步:再进入维护界面,选择版本升级 第三步:进入产品密钥,输入密钥 第四步:一直点下一步,直到升级完毕. SQL Server 2008 Developer:PTTFM-X467G-P7RH2-3Q6 ...
  • 诺基亚手机使用的Windows Phone系统一直被称作安全性最好的手机,早在Windows Phone 7的时代就一直是Windows Phone设备黑客们难解的问题,而升级到Windows Phone 8之后,越狱和破解的难度又进一步增大了,除了三星的 Ativ S 之外几乎所有的 Window ...
  • 文/王冠雄携程"泄露门"是中国网络支付的一次标志性安全事故.这次波及全国的信用卡信息大泄露,为日益增长的网络支付市场敲响了警钟.在线旅行网站为了测试和运营方便,就敢存储用户CVV2信息.那么,还有哪些地方安放着我们的隐私?相比国外,我们还需要做很多.没有完美的法律,法律之外要靠自 ...
  • 1.旋转函数:    glRotatef(float angle, float X, float Y, float Z) 其中,angle指定对象旋转的角度,X,Y,Z三个参数共同决定旋转轴的方向. 即,glRotatef函数是将某对象沿指定轴旋转angle角度.   2.旋转实现方法:     f ...
  • 上机目的: 学会利用导入的方法创建数据库并用SQL完成查询. 上机内容及步骤: 现有图书管理数据库的三个关系模式: 图书(总编号,分类号,书名,作者,出版单位,单价) 读者 (借书证号,单位,姓名,性别,职称,地址) 借阅 (借书证号,总编号,借书日期) 一.将word中的下列三个表D:\SQL\图 ...
一周排行
  • 其实大大小小的文章讨论程序员的人生规划不计其数.本人还是坚持谈谈个人对于程序员日后规划的看法,也是本人做事的一种风格跟对人生的态度吧.希望能给大家有点帮助. 首先我们庆幸的是我们是软件行业的一员,我们作为程序员从最基 ...
  • Google Chrome 9正式迈入稳定版本,支援WebGL硬体加速3D功能.Chrome Instant即搜即看与Chrome Web Store线上软件商店.另外根据安全厂商VUPEN指出,Chrome 9修正 ...
  • 之前一直比较习惯用Ext.apply()方法来实现对象的克隆,今天遇到一个问题,当对象中含有数组,且数组中包含复杂类型时,Ext.apply()的克隆就有问题了. 于是就想着试试自己能不能解决.在网上查了一会儿,发现 ...
  • 目前比较主流的移动设备系统包括 Android,IOS ,Symbian,BlackBerry 与Web OS.这些系统浏览器都是基于webkit核心,而webkit号称是一款全功能的移动浏览器,支持 HTML + ...
  •   这学期学了计算机学院王彦老师的计算机安全概论,算是一门入门级的课程,但是自己还是长了很多的见识的,学到缓冲区溢出攻击的时候,突然想到自己拖了很久的一件事情,就是好好的总结和学习一下C语言缓冲区的问题.时间过了这么 ...
  • 遇到此错误,我先备份了所有邮件.然后打开Outlook的文件菜单,文件数据管理,ost文件路径处删除所有文件(通讯录文件夹除外).然后重新新建了一个Outlook配置文件(控制面板,邮件).然后问题搞定.其原因,应该 ...
  • 企业java应用的性能调优是一项艰巨的.有时甚至是徒劳的任务,这是由现代应用的复杂性和缺少正规的调优方法导致的.现代企业应用与十年前的应用相比差 距很大,如今这些应用支持多输入.多输出.复杂的框架和业务处理引擎.而十 ...
  • 一天正在发呆,QQ上的一个朋友向我求救:"我的网站被黑了,首页给换了,SOS!".最近正好无事,索性就帮帮他吧. 收复失地 刚刚准备在浏览器上输入他网站的地址,结果却停了下来:如果入侵者在首页挂了 ...
  • 这周工作有点儿累了,写个文章放松一下,就写个最最原始的时候,我是如何学习一门开发语言的步骤.    1. 我往往是被动学习,就是社会上流行什么了,我就学什么,因为我外语不太好,不大知道前沿的技术,也看不懂前沿的技术资 ...
  • 易网科技报道 4月19日讯 甲骨文控告Google侵权的案件于周一(4/17)正式开庭,这场和解不成最后迈入审判的官司周二(4/17)传唤双方CEO出庭作证,就Android是否侵犯甲骨文的Java专利提出证词.先上 ...