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

2022-07-27 15:42:30 浏览数 (1)

公众号中关于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套接字的步骤图,可以不细究:

0 人点赞