socket函数
为了执行网络I/O,一个进程必须做到第一件事情就是调用socket函数,指定期望的通信协议类型(使用IPv4的TCP、使用IPv6的UDP、Unix域字节流协议等)
#include<sys/socket.h> int socket(int family,int type,int protocol); //返回:若成功则为非负描述符,若出错则为-1
其中family参数指明协议族,它是图4-2中所示的某个常值。该参数也往往被称为协议域。type参数指明套接字类型,它是图4-3中所示的某个常值。protocol参数应设为图4-4所示的某个协议类型常值,或者设为0,以选择所给定family和type组合的系统默认值。
并非所有套接字family与type的组合都是有效的,图4-5给出了一些有效的组合和对应的真正协议。其中标为 “是”的项也是有效的,但还没有找到便捷的缩略词,而空白项则是无效组合。
4-1基本TCP客户/服务程序的套接字
socket函数在成功返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符(socket descriptor),简称sockfd。为了得到这个套接字描述符,我们只是指定了协议族(IPv4、IPv6或Unix)和套接字类型(字节流、数据报或原始套接字)。我们并没有指定本地协议地址或远程协议地址。
对比AF_xxx和PF_xxx
AF_前缀表示地址族,PF_前缀表示协议族。
在POSIX规范指定socket函数的第一个参数为PF_值,而AF_值用于套接字地址结构,然而它在addrinfo结构中却只定义一个族值,既用于调用socket函数,也用于套接字地址结构中
connect函数
TCP客户用connect函数来建立与TCP服务的连接。
#include<sys/socket.h> int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen); //返回:若成功则为0,若出错则为-1
sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小,套接字地址结构必须含有服务器的IP地址和端口号。
客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口号作为源端口。
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况。
(1)若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误,举例来说,调用connect函数时,4.4BSD内核发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再法一个,若总共等了75s后仍未收到响应则返回本错误。
有些系统提供对超时值的管理性控制。
(2)若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们制定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接受到RST就马上返回ECONNREFUSED错误。
RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器:TCP想取消一个已有连接:TCP接收到一个根本不存在的连接上的分节。
(3)若客户发出的SYN在中间的某个路由器上引发一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该信息,并按第一种情况中所述的时间间隔继续发送SYN,若在某个规定时间后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下两种情形也是由可能的:一种是按照本地系统的转发表,根本没有到达远程系统的路径;二是connect调用根本不等待就返回。
bind函数
bind函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
#include<sys/socket.h> int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen); //返回:若成功则为0,若出错则为-1
第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。
listen函数
listen函数仅由TCP服务器调用,它做两件事。
(1)当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态换到LISTEN状态。
(2)本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
#include<sys/socket.h> int listen(int sockfd,int backlog); //返回:若成功则为0,若出错则为-1
本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列;
(1)未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并发到服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
(2)已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中的一项。这些套接字处于ESTABLISHED状态
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。
accpet函数
accpet函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)
#include<sys/socket.h> int accept(int aockfd,struct sockaddr *cliaddr,socklen_t *addrlen); //返回:若成功则为非负描述符,若出错则为-1
参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。addrlen是值-结果参数,调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
如果accpet成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。在讨论accpet函数时,我们称它的第一个参数为监听套接字(listen sicket)描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字(connected socket)描述符。区分这两个套接字非常重要。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。
本函数最多返回三个值:一个既可能是新套接字描述符也可能是出错指示的整数、客户进程的协议地址(cliaddr指针所指)以及该地址的大小(由addrlen指针所指)。如果我们对返回客户协议地址不感兴趣,那么可以把cliaddr和addrlen均值为空指针。
fork和exec函数
#include<unistd.h> pid_t fork(void); //返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
如果你以前从未接触过该函数,那么理解fork最困难之处在于调用它一次,它却返回两次。它在调用进程(称为父进程)中返回一次,返回值是新派生进程(称为子进程)的进程ID号;在子进程又返回一次,返回值为0.因此,返回值本身告知当前进程是子进程还是父进程。
fork在子进程返回0而不是父进程的进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以调用getppid取得父进程的ID。相反,父进程可以有许多子进程,而且无法获取各个子进程的 进程ID。如果父进程想要跟踪所有子进程的进程ID,那么它必须记录每次调用fork的返回值。
父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。我们将看到网络服务器利用了这个特性:父进程调用accpet之后调用fork。所接受的已连接套接字随后就在父进程与子进程之间共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。
fork有两个典型用法。
(1)一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法 。
(2)一个进程想要执行另一个程序。既然创建新进程的唯一方法是调用fork,该进程于是首先调用fork创建一个自身的副本,然后创建新进程的唯一方法是调用fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec(接下去介绍)把自身替换成新的程序。这是诸如shell之类程序的典型用法。
存放在硬盘上的可执行程序文件能够被Unix执行的唯一方法是:由一个现有进程调用六个exec函数中的某一个(当这6个函数中是哪一个被调用并不重要时,我们往往把他们统称为exec函数。)exec把当前进程映像替换成新的程序文件,而且该新程序通常从main函数开始执行。进程ID并不变。我们称调用exec的进程为调用进程,称新执行的程序为新程序。
这6个exec函数之间的区别在于(a)待执行的程序文件是由文件名还是由路径名指定;(b)新程序的参数是一一列出还是由一个指针数组来引用;(c)把调用进程的环境传递给新程序还是给新程序指定新的环境。
close函数
通常的Unix close函数也常用来关闭套接字,并终止TCP连接。
#include<unistd.h> int close(int sockfd); //返回:若成功则为0,若出错则为-1
close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
getsockname和getpeername函数
这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地协议地址(getpeername)
#include<sys/socket.h> int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen); int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addrlen); //均返回:若成功则为0,若出错则为-1