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