前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >3-UNIX网络编程-读写数据

3-UNIX网络编程-读写数据

作者头像
zoujunjie202
发布2022-07-27 15:42:30
4740
发布2022-07-27 15:42:30
举报
文章被收录于专栏:zoujunjie202

公众号中关于Unix网络编程的1、2章节对基础知识做了铺垫,介绍了建立网络通信的API。然而客户和服务器之间建立通信管道(以下简称Channel)之后,如何管理Channel以及Channel中双向流动的数据才是开发者关注的重点,这构成了所有网络应用(如http服务器,ftp服务器等)的基础,也才真正是Unix网络课程这个分支所涉及的内容。

write和read

如上图,是1、2章节的数据流示意图。linux内核提供了对Channel的读写API,翻看前面的代码可以看到使用方法。我们先看看write和read api的函数声明。

代码语言:javascript
复制
ssize_t write(int filedes,const void *buf,size_t nbytes);
#include <unistd.h>
write函数向filedes中写入nbytes字节数据,数据来源为buf。
返回值:一般等于nbytes,否则表示出错
代码语言:javascript
复制
ssize_t read(int filedes,void *buf,size_t nbytes);
#include <unistd.h>
read函数从filedes指定的已打开文件中读取nbytes字节到buf中。
返回值:读取到的字节数,0代表读到EOF,-1代表出错。

在套接字socket上,write和read的行为跟文件读写的行为有点差异。在Socket Channel上有缓冲机制,当缓冲区被写满时,单次读写的数据就是不定长的,这时候需要多次调用读写。本来想找一个例子来展示这个出错场景,发现前面两个章节的Demo没法展示,只能留在将来有足够条件的时候再论证了。

为了解决这个问题,可以引入以下两个包裹函数:

代码语言:javascript
复制

ssize_t readn(int fd , void *vptr, size_t n){
    size_t nleft ;
    ssize_t nread;
    char *ptr;
    ptr = vptr;  // 如果不把vptr的地址赋给新值,多次读取时不方便移动指针
    nleft = n;
    while (nleft > 0) {
        if( ( nread = read(fd,ptr,nleft)) < 0 ){
            if( errno == EINTR ) // 处理尝试读取数据但被系统打断的情况
                nread = 0 ; // 标记读取了0个字节,并再次尝试阻塞到read api
            else
                return (-1);
        }else if( nread == 0){
            break ; // 结尾
        }
        nleft = nleft - nread ;
        ptr = ptr + nread ;
    }
    return n - nleft ;
}
代码语言:javascript
复制
ssize_t writen(int fd ,const void *vptr, size_t n){
    size_t nleft;
    ssize_t nwritten ;
    const char *ptr;
    ptr = vptr ;
    nleft = n ;
    while ( nleft > 0 ) {
        if( (nwritten = write(fd, ptr, nleft)) <= 0 ){
            if( nwritten < 0 && errno == EINTR ){
                nwritten = 0 ;
            }else{
                return (-1);
            }
        }
        nleft = nleft - nwritten ;
        ptr = ptr + nwritten ;
    }
    return (n);
}

至此,文章开头给的数据流图可以变成这样:

【备注】这两个函数会循环读取socket中的内容,如果读取的内容为空还会阻塞进程,在很多情况下应该要有结束符来终止读取。

readline函数

前面的包裹函数readn是按指定长度nbytes来读取数据,但是在日常使用场景里面,更多是以结束符来判断字节流的结束。所以为了以后使用,我们添加一个readline函数。

代码语言:javascript
复制

ssize_t readline(int fd , void *vptr, size_t maxlen){
    ssize_t n , rc ;
    char c,*ptr;
    ptr = vptr ;
    for ( n = 1 ; n < maxlen; n++ ) {
    again:
        if( (rc=read(fd, &c , 1)) == 1 ){
            *ptr++ = c ; // 报错读到的字符
            if( c == '\n' )
                break;  // 读到换行符,结束读取
        }else if( rc == 0 ){
            *ptr = 0 ; //读取到文件末尾,直接退出,并返回读到的字符总数
            return ( n - 1 );
        }else{
            if( errno == EINTR ) // 系统中断
                goto again; // 可以 n-- 之后直接 continue,避免跳转
            return (-1);
        }
    }
    *ptr = 0;
    return (n) ;
}

网络传输细节

上面提到的函数实际上是处理应用层数据的,而传输层、网络层、数据链路层又如何处理数据的呢?显然继续往下深究的话,会是很多个章节的事情,而且我自己也没有动力继续看物理层的工作细节。以《UNIX网络编程》这本书籍作为基础,稍作整理。

如上图,表示应用程序写TCP套接字时涉及的步骤和缓冲区。由上至下列举几个重点:

1、用户进程缓冲区:通常是内存,由应用程序自己管理,所以大小是任意指定。

2、write:用户态存放在内存中的数据,通过write API往套接字缓冲区写,缓冲区满时,write API阻塞并等待缓冲区可写信号。

3、套接字发送缓冲区:由SO_SNDBUF指定,默认情况下在8192至61440之间,推荐的设置值是 (4+2*n)*MSS,就是MSS的4倍以上,且为偶数倍。

4、MSS:macimum segment size,TCP在握手环境告知对方的最大TCP分节大小。在SYN分节上有体现,是经过双方协商之后的值,商定的值利于减少网络传输时数据分片。通常 MSS ≤ MTU – 40 (IPv4) 或 MTU – 60(IPv6) 。

5、TCP分节,通信双方协商MSS大小后,把缓冲区的数据按MSS大小进行分割,提交给IP协议进行处理。

6、MTU:maximum transmission unit,最大传输单元,由网络环境中的硬件进行规定,MTU的大小决定了IP包的处理方式,IPv4需要的最小MTU为68字节,IPv6则需要1280字节。以太网环境的MTU为1500字节,但是不代表IP包就可以不经任何处理即可发送,因为数据传输要经过N个物理节点,N个物理节点中的最小MTU决定了IPv4的主机要不要对IP包进行分片。

以上内容仅仅为了满足好奇心而整理,实际应用还需要很多的阅读和实践,所以有个大概了解即可,多数场景下都用不到。文章结尾再贴一个写UDP套接字的步骤图,可以不细究:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 zoujunjie202 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档