一般认为Web服务器程序是一个长时间运行的程序(即所谓的守护进程,daemon ),它只响应来自网络的请求时才发送网络消息。协议的另一端是Web客户程序,如某种浏览器,与服务器进行通信总是由客户进程发起。在设计网络应用时,确定总是由客户发起请求往往能够简化协议和程序本身。当然一些较为复杂的的网络应用还需要异步回调通信,也就是由服务器向客户端发起请求信息。
客户与服务器之间是通过某个网络协议通信的,但实际上,这样的通信通常涉及多个网络协议层。这里聚焦的是:TCP/IP协议族,也称为网络协议族。举例来说,Web客户与服务器之间使用TCP通信,TCP又转而使用IP通信,IP再通过某种形式的数据链路层通信。
在图中,客户与服务器之间的信息流在其中一端是向下通过协议栈的,跨越网络后,在另一端则是向上通过协议栈的。另外注意,TCP/IP协议是内核中协议栈的一部分。(回忆:在LINUX的进程中,LINUX系统是在内核态的,内核态被所有的进程所共享,而TCP/IP协议属于LINUX系统的网络协议,也就是内核中的协议栈,具体的linux内核相关的学习在linux 系统文集中)
同一网络应用的客户和服务器当处于不同局域网时,不同的局域网使用路由器连接到广域网:
根据原书的介绍,代码里面所使用的大多数系统函数,都定义了各自的包裹函数。而且可以使用这些包裹函数来检查错误,输出适当的消息,以及在出错时终止程序的运行。
下面讲代码了,这个是客户端显示服务器的当前时间和日期的简单代码(base1)
#include "unp.h" //该头文件包含大部分网络程序都需要的许多系统头文件,并定义了所用到的各种常量值(如MAXLINE).
int main(int argc, char **argv) // main 函数的定义,形参是命令行参数
{
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2) err_quit("usage: a.out <IPaddress>");
// 调用 socket 函数创建一个 ipv4 字节流套接字,返回一个小整数描述符,以后的所有函数调用(如随后的connect 和 read)就用该描述符来标识这个套接字。
// 如果socket函数调用失败,我们就调用自己的err_sys 函数放弃程序运行。
// 自定义的 **err_sys 函数** 输出我们作为参数提供的出错信息以及所发生的系统错误的描述。后面会具体描述这些函数
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error");
// 使用bzero把整个结构清零后,置地址族为AF_INET,端口号为13.
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr)<= 0) err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr))< 0) err_sys("connect error");
// 两个左括号间加一个空格,提示比较运算符的左侧同时也是一个赋值运算
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
}
if (n < 0) err_sys("read error");
exit(0);
}
细节
socket模块定义了一些常量参数,用来指定 socket的的地址族、socket的类型、以及支持的TCP/IP协议。
socket.socket([family[, type[, proto]]]):根据指定的 地址族 和 套接字类型、协议编号(默认为0)来创建 套接字对象。AF_INET 对应的 IPV4, AF_INET6 对应的 IPV6。具体参数见下表:
1、我们把服务器的IP地址和端口号填入一个网际套接字地址结构(一个名为 servaddr 的 sockaddr_in 结构变量)。这里,端口号为13,是时间获取服务器的众所周知端口,支持该服务器的任何 TCP/IP 主机都使用这个端口号。
2、而网际套接字地址结构中 IP地址 和 端口号 这两个成员必须使用特定格式,为此我们调用库 htons(“主机到网络短整数”)去转换二进制端口号,又调用库函数 inet_pton(“呈现形式到整数”)去把 ASCII命令行参数(例如运行本例子所用的206.168.112.96)转换为合适的格式。
3、bzero 不是一个ANSI C函数,几乎所有支持套接字API的厂商都提供bzero,如果没有,那么可以使用unp.h头文件中提供的该函数的宏定义。inet_pton 函数是一个支持 IPV6 的新函数,以前的代码使用 inet_addr 函数来把ASCII点分十进制数串变换为正确的格式,不过它有不少局限,而这些局限在inet_pton中都得以纠正。
4、connect 函数应用于一个TCP套接字时,将与由它的第二个参数指向的套接字地址结构指定的服务器建立一个TCP连接。该套接字地址结构的长度也必须作为该函数的第三个参数指定,对于网际套接字地址结构,我们总是使用C语言的sizeof操作符由编译器来计算这个长度。
5、在头文件unp.h中,我们使用#define 把SA定义为struct sockaddr,通用套接字地址结构。每当一个套接字函数需要一个指向某个套接字地址结构的指针时,这个指针必须强制类型转换成一个指向通用套接字地址结构的指针。
6、fputs()函数用于将指定的字符串写入到文件流中,其原型为:
int fputs(char * string, FILE * stream);【参数】string为将要写入的字符串,stream为文件流指针。【返回值】成功返回非负数,失败返回EOF。fputs()从string的开头往文件写入字符串,直到遇见结束符 '\0','\0' 不会被写入到文件中。注意:fputs()可以指定输出的文件流,不会输出多余的字符;puts()只能向 stdout 输出字符串,而且会在最后自动增加换行符。
协议无关性
上面的程序是与 IPv4 协议相关的:我们分配并初始化一个 sockaddr_in 类型的结构,把该结构的协议族成员设置为AF_INET, 并指定 socket 函数的第一个参数为 AF_INET.
为了让图1-5的程序能够在IPv6上运行,我们必须修改这段代码。下面是一个能在IPV6上运行的版本(base2):
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in6 servaddr;
char recvline[MAXLINE + 1];
if (argc != 2) err_quit("usage: a.out <IPaddress>");
if ( (sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin6_family = AF_INET6;
servaddr.sin6_port = htons(13); /* daytime server */
if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0) err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error");
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF) err_sys("fputs error");
}
if (n < 0) err_sys("read error");
exit(0);
}
不足之处:
- 这种写法是与ipv6协议相关的写法,更好的做法是编写协议无关的程序。
- 这里的不足之处,用户必须以点分十进制数格式给出服务器的IP地址(如适合于IPV4版本的206.168.112.219)。而我们一般都习惯于用名字代替数字。
错误处理:包裹函数
任何现实世界的程序都必须检查每个函数调用是否返回错误。在上面的程序中,我们检查socket、inet_pton、connect、read 和 fputs函数是否返回错误,当发生错误时,就调用我们自己的err_quit 或 err_sys 函数输出一个出错信息并终止程序的运行。但是个别情况下,当这些函数返回错误时,我们想做的事并非简单地终止程序的运行。
于是,我们通过定义包裹函数来缩短程序。每个包裹函数完成实际的函数调用,检查返回值,并在发生错误时终止进程。我们约定包裹函数名是实际函数名的首字母大写形式。例如在语句:
sockfd = Soccket(AF_INET,SOCKET_STREAM,0); 中,函数Socket 是函数 socket的包裹函数,如图:
/* include Socket */
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
err_sys("socket error");
return(n);
}
线程函数遇到错误时并不设置标准Unix的errno变量,而是把errno的值作为函数返回值返回调用者。这意味着每次调用以pthread_开头的某个函数时,我们必须分配一个变量来存放函数返回值,以便在调用err_sys前把errno变量设置成该值。
/* include Pthread_mutex_lock */
void Pthread_mutex_lock(pthread_mutex_t *mptr)
{
int n;
if ( (n = pthread_mutex_lock(mptr)) == 0) return;
errno = n;
err_sys("pthread_mutex_lock error");
}
/* end Pthread_mutex_lock */
除非必须检查某个确定的错误是否发生,并以不同于终止进程的其他某种方式处理它,否则就使用这些包裹函数。
下面是匹配的时间获取服务器程序(base3)
#include "unp.h"
#include <time.h>
int main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
// 调用 socket 函数调用一个 ipv4 字节流套接字,返回一个小整数描述符
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 填写网际套接字地址结构并调用bind函数
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET
// 我们指定IP地址为INADDR_ANY,这样要是服务器主机有多个网络接口,服务器进程就可以在任意
// 网络接口上接受客户连接。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
// 调用 listen 函数把该套接字转换成一个监听套接字,这样来自客户的外来链接就可在该套接字上由内核接受。
// 常值 LISTENNQ 在我们的 unp.h 头文件中定义。它指定系统内核允许在这个监听描述符上排队的最大客户。
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL);
ticks = time(NULL);
// snprintf 函数在这个字符串末尾添加一个回车符和一个换行符,随后write函数把结果字符串写给用户。
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
细节:
1、通常情况下,服务器进程在accept调用中被投入睡眠,等待某个客户连接的到达并被内核接受。TCP连接使用所谓的三次握手来建立连接。握手完毕时accept返回,其返回值是一个称为已连接描述符的新描述符(本例中为connfd)。该描述符用于与新近连接的那个客户通信。accept 为每个连接到本服务器的客户返回一个新描述符。
2、调用sprintf无法检查目的缓冲区是否溢出,相反,snprintf要求其第二个参数指定目的缓冲区的大小,因此可确保该缓冲区不溢出。
3、值得注意的是:许多网络入侵是由黑客通过发送数据,导致服务器对sprintf的调用使其缓冲区溢出而发生的,必须小心使用的函数还有gets、strcat和strcpy,通常应分别改为调用fgets、strncat和strncpy。更好的替代函数是后面才引入的strlcat 和 strlcpy,它们确保结果是正确终止的字符串。
4、本服务器一次只能处理一个客户。如果多个客户连接差不多同时到达,系统内核在某个最大数目的限制下把它们排入队列,然后每次返回一个给accept函数。本服务器只需调用time 和 ctime 这两个库函数,运行速度很快。同时能处理多个用户的并发服务器有多种编程写法,最简单的技术是Unix的fork函数(多进程编程),或在服务器启动时预先fork一定数量的子进程(进程池)。
之后,全部用来描述网络编程中使用的各种技术的两个客户/服务器程序示例如下:
- 时间获取客户/服务器程序(base1, base2, base3);
- 回射客户/服务器程序(base4)
所有程序的扩展,以及所有程序的完善,都是与这三个程序息息相关。
描述一个网络中各个协议层的常用方法是使用国际标准化组织的**计算机通信开放系统互联(OSI)模型。这是一个七层模型,如下图所示,图中同时给出了它与网际协议族的近似映射。
这里,OSI 模型的底下两层是随系统提供的 设备驱动 和 网络硬件。通常情况下,除需知道数据链路的某些特性外,我们不必要知道这两层的具体情况。
图中,TCP和UDP之间留有空隙,表明:网络应用绕过传输层直接使用 IPv4 或 IPv6 是可能的。这就是所谓的原始套接字。
OSI模型的顶上三层被合并成一层,称为应用层。这就是:Web客户(浏览器)、Telent客户、Web服务器、FTP服务器和其他我们在使用的网络应用所在的层。而进行网络编程的套接字就是从应用层进入传输层的接口。重点是,如何使用套接字编写使用TCP或UDP的网络应用程序。 后面还会有如何通过原始套接字彻底绕过IP层直接读取数据链路层的帧。
问:之所以套接字提供的是从OSI模型的顶上三层进入传输层的接口?
这样设计有两个理由,理由之一是:顶上三层处理具体网络应用(如FTP、Telnet或HTTP)的所有细节,却对通信细节了解很少;底下四层对具体忘了应用了解不多,却处理所有的通信细节:** 发送数据,等待确认,给无序到达数据排序,计算并验证校验和,等等。理由之二:顶上三层通常构成所谓的用户进程,底下四层却通常作为操作系统内核的一部分提供。Unix与其他现代操作系统都提供分隔用户进程与内核的机制。由此可见,OSI的第四层和第五层的接口是构建API的自然位置。
网络拓扑的发现
大多数Unix系统都提供了可用于发现某些网络细节的两个基本命令:netstat 和 ifconfig。而且,有些厂商把这些命令存放在诸如/sbin 或 /usr/sbin 这样的管理目录中,而不是通常的/usr/bin目录,而这些管理目录可能不在通常的shell搜索路径中(由PATH环境变量指定)
(1)netstat -i 提供网络接口的信息。我们还指定 -n标志以输出数值地址,而不是试图把它们反向解析成名字。下面的例子给出了接口及其名字好统计信息:
其中环回(loopback)接口称为lo,以太网接口称为eth0。下面的例子给出了支持IPV6的一个主机的类似信息:
(2)netstat -r展示路由表,也是另一种确定接口的方法。我们通常指定-n标志以输出数值地址。它还给出默认路由器的IP地址。
(3)有了各个网络接口的名字,执行ifconfig就可获得每个接口的详细信息。
该命令给出了指定接口的IP地址、子网掩码和广播地址。其中的MULTICAST标志通常指明该接口所在主机支持多播。有些ifconfig的实现还提供-a标志,用于输出所有已配置接口的信息。
(4)找出本地网络中众多主机的IP地址的方法之一是,针对从上一步找到的本地接口的广播地址执行ping命令。
POSIX 的背景
POSIX(可移植操作系统接口)是由IEEE开发的一系列标准。第一个标准详述了进入类Unix内核的C语言接口,涵盖了下述领域:进程原语(fork、exec、信号和定时器)、进程环境(用户ID和进程组)、文件与目录(所有I/O函数)、终端I/O、系统数据库(口令文件和用户组文件)以及tar和cpio归档格式。POSIX.1增添了3章关于线程的内容,并另有关于线程同步(互斥锁和条件变量)、线程调度和同步调度的各节。其中,声明 ISO/IEC 9945 由下面3个部分构成:
- Part 1: System API(C language)—— 第一部分:系统API(C语言)。
- Part 2: Shell and utilities—— 第二部分:Shell 和实用程序。
- Part 3: System administration—— 第三部分:系统管理(正在开发中) 。
最后一个版本,是联网API标准,定义了两个API,并称它们为详尽网络接口(DNI)
- DNI/Socket,基于 4.4BSD 的套接字API
- DNI/XTI,基于 X/Open 的 XPG4规范
** 64位体系结构**
选用64位软件的体系结构,原因之一是在每个进程内部可以由此使用更长的编址长度(即64位指针),从而可以寻址很大的内存空间(超过2^32字节)。现有32位Unix系统上共同的编程模型称为ILP32模型,表示整数(I)、长整数(L)和指针(P)都占用32位。64位Unix系统上变得最为流行的模型称为LP64模型,表示只有长整数(L)和指针(P)占用64位。下面对这两种模型进行了比较。
ANSI C创造了 size_t 数据类型,它用于作为malloc的唯一参数(待分配的字节数),或者作为read 和 write的第三个参数(待读或写的字节数)。在32位系统中 size_t 是一个 32位值,但是在64位系统中它必须是一个64位值,以便发挥更大寻址模型的优势。也就意味着64位系统中也许含有一个把size_t定义为unsigned long 的typedef指令。联网API存在如下问题:POSIX.1g的某些草案规定,存放套接字地址结构大小的函数参数具有size_t数据类型(如bind和connect的第三个参数)。如果不修改这些规定,当Unix系统从ILP32模型转变为LP64模型时,size_t和long都将从32位值变为64位值。这两个例子实际上并不需要使用64位的数据类型:套接字地址结构的长度最多也就几百个字节。处理这些情况的办法是使用专门设计的数据类型。套接字API对套接字地址结构使用 socklen_t 数据类型。不把这些值由32位改为64位的理由是易于为那些已在32位系统中编译的应用程序提供在新的64位系统张的二进制代码兼容性。
(注明1:守护进程不仅仅是一个长时间运行的程序,而是一个随着计算机启动,而自动运行的后台程序,而且能在后台运行且不跟任何终端关联的进程——运行,一般用shell去写一个自定义的守护进程)
(注明2:异步回调:回调就是该函数写在高层,低层通过一个函数指针保存这个函数,在某个事件的触发下,低层通过该函数指针调用高层那个函数。异步区别于同步,在同步模式下,一段代码调用另一段代码时,只能采用同步调用,必须等待这段代码执行完返回结果后,调用方才能继续往下执行,有了多线程的支持,可以采用异步调用,调用方和被调方可以属于两个不同的线程,调用方启动被调方线程后,不等对方返回结果就继续执行后续代码。被调方执行完毕后,通过某种手段通知调用方:结果已经出来,请酌情处理。,这里的某种手段主要是指的线程间的通信:管道,socket)
(注明3:C语言中用#define伪命令定义的对象称为常数,用const限定词定义并初始化的对象称为常量)。常数的值在编译时确定,常量的值则在运行时初始化后确定(不过此后只能作为右值使用)。本书绝大多数恒定值是用#define 定义的常数。)
(注明4:Unix errno 值,只要一个Unix函数(例如某个套接字函数)中有错误发生,全局变量errno就被置为一个指明该错误类型的正值,函数本身则通常返回-1。err_sys 查看errno变量的值并输出相应的出错消息,例如当errno值等于ETIMEOUT时,将输出“Connection time out”(连接超时)。errno的值只在函数发生错误时设置。如果函数不返回错误,errno的值就没有定义。errno 的左右正数错误值都是常值,具有以“E”开头的全大写字母名字,并通常在<sys/errno.n> 头文件中定义。值0不表示任何错误。)
(注明5:对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或create返回的文件描述符表示该文件,将其作为参数传给read或write函数。用size_t作为参数的几个API函数如下:
- malloc 向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以通过类型转换强制转换为任何其它类型的指针。
#include <stdlib.h>
#include <malloc.h>
extern void* malloc(unsigned int num_bytes); // 函数声明:void *malloc(size_t size);
- read函数定义如下:
#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回读到的字节数,若已到文件末尾则返回0,若出错则返回-1
// filedes:文件描述符
// buf:读取数据缓存区
// nbytes:要读取的字节数
// 有几种情况可使实际读到的字节数少于要求读的字节数:
// 1)读普通文件时,在读到要求字节数之前就已经达到了文件末端。例如,若在到达文件末端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将返回0(文件末端)。
// 2)当从终端设备读时,通常一次最多读一行。
// 3)当从网络读时,网络中的缓存机构可能造成返回值小于所要求读的字结束。
// 4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
// 5)当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录。
// 6)当某一个信号造成中断,而已经读取了部分数据。
case:
// 设置读取的长度:
char msg[1024];
// 读取用户输入:
int ret = read(fd, msg, sizeof(msg));
if( ret < 0 )
{
perror("read fail ");
exit(1);
}
- write函数定义如下:
#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回写入的字节数,若出错则返回-1
// filedes:文件描述符
// buf:待写入数据缓存区
// nbytes:要写入的字节数
case:
void TcpEventServer::ListenerEventCb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data)
{
TcpEventServer *server = (TcpEventServer*)user_data;
//随机选择一个子线程,通过管道向其传递socket描述符
int num = rand() % server->m_ThreadCount;
int sendfd = server->m_Threads[num].notifySendFd;
write(sendfd, &fd, sizeof(evutil_socket_t));
}
)
(注明5:read/write的语义:为什么会阻塞?http://www.cnblogs.com/xiehongfeng100/p/4619451.html
首先,write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知。之所以会阻塞,是当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有自己的send buffer和receive buffer。从Linux 2.6开始,两个缓冲区大小都由系统自动调节,但一般都在default和max之间浮动。
# 获取socket的发送/接受缓冲区的大小:(后面的值是在Linux 2.6.38 x86_64上测试的结果)
sysctl net.core.wmem_default #126976
sysctl net.core.wmem_max #131071
已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口和拥塞控制,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞。而read调用的行为相对容易理解,从socket的receive buffer中拷贝数据到应用程序的buffer中。read调用阻塞,通常是发送端的数据没有到达。)
(注明6:blocking(默认)和nonblock模式下read/write行为的区别
将socket fd设置为nonblock(非阻塞)是在服务器编程中常见的做法,采用blocking IO并为每一个client创建一个线程的模式开销巨大且可扩展性不佳(带来大量的切换开销),更为通用的做法是采用线程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。
// 设置一个文件描述符为nonblock
int set_nonblocking(int fd)
{
int flags;
if ((flags = fcntl(fd, F_GETFL, 0)) == -1)
flags = 0;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
几个重要的结论:
read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。
只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)注:阻塞模式下,当对方socket关闭时,read会返回0。blocking 的 write 只有在缓冲区足以放下整个 buffer 时才返回(与blocking read并不相同)nonblock write
则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer)
)
(注明7:read/write对连接异常的反馈行为
对应用程序来说,与另一进程的TCP通信其实是完全异步的过程:
- 我并不知道对面什么时候、能否收到的数据
- 我不知道什么时候能够收到对面的数据
- 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)
对于1和2,采用write() -> read() -> write() -> read() ->...的序列,通过blocking read或者nonblock read+轮询的方式,应用程序基于可以保证正确的处理流程。
对于3,kernel将这些事件的“通知”通过read/write的结果返回给应用层。
)