图4-1给出了在一对TCP客户与服务器进程之间发生的一些典型事件的时间表。服务器首先启动,稍后某个时刻客服启动,它试图连接到服务器。我们假设客户给服务器发送一个请求,服务器处理该请求,并且给客户发回一个响应。这个过程一直持续下去,直到客户关闭连接的客户端,从而给服务器发送一个EOF(文件结束)通知为止。服务器接着也关闭连接的服务器端,然后结束运行或者等待新客户连接。
注意:struct sockaddr
#define SA struct sockaddr
也就是通用套接字地址结构.每当一个套接字函数需要一个指向套接字地址结构的指针时,这个指针必须强制类型转成一个指向通用套接字地址结构的指针。这是因为套接字函数早于ANSI C标准,当时void *指针类型还不可用.
#include
/* 返回:若成功则为非负描述符,若出错则为-1 */
int socket(int family, int type, int protocol);
family | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
… | … |
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
… | … |
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
TCP客户用connect函数来建立与TCP服务器的连接。
#include
/*
* 返回:若成功则为0,若出错则为-1
*
* 第二个参数,第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
* 套接字地址结构必须包含有服务器的IP地址和端口号。
*/
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
客户在调用函数connect前不必非得调用bind函数,因为需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在成功或错误时才退出,其中出错返回可能有以下几种情况:
bind函数把一个本地协议地址赋予给一个套接字。
#include
/*
* 返回: 若成功则为0,若出错则为-1
*
* 第二个参数是一个指向特定的地址结构的指针
* 第三个参数是该地址结构的长度。
*/
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以不指定。如果哦服务器指定,则只接受那些目的地为该IP地址的客户连接。
IP地址 | 端口 | 结果 |
---|---|---|
通配地址 | 0 | 内核选择IP地址和端口 |
通配地址 | 非0 | 内核选择IP地址,进程指定端口 |
本地IP地址 | 0 | 进程指定IP地址,内核选择端口 |
本地IP地址 | 非0 | 进程指定IP地址和端口 |
需要注意的是,如果让内核来为套接字选择一个临时端口,函数bind并不返回所选择的值,必须调用getsockname
来返回协议地址。
#include
/*
* 返回: 若成功则为0,若出错则为-1
*
* 第二个参数规定了内核应该为相应套接字队列的最大连接个数。
* http://man7.org/linux/man-pages/man2/listen.2.html
* The backlog argument defines the maximum length to which the queue of
* pending connections for sockfd may grow.
*/
int listen(int sockfd, int backlog);
如何理解backLog参数?我们必须认识到内核为任何一个给定的监听套接字维护2个队列:
accept函数由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接(图4-7)。如果已完成队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞模式)。
#include
/*
* 返回:若成功则为非负描述符,若出错则为-1
*
* 参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。
* addrlen是值-结果参数:
* 调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,
* 返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
*
* accept的第一个参数为监听套接字,返回值为已连接套接字。
*
* 本函数最大返回三个值:
* 一个即可能是新套接字描述符也可能是出错指示的整数,客户进程的协议地址(由cliaddr指
* 针所指)以及该地址的大小(由addrlen指针所指)。如果对返回的客户地址不感兴趣,那么
* 可以把cliaddr和addrlen均置为空指针。
*/
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
#include "unp.h"
#include
int
main(int argc, char **argv) {
/*
* len: 它将成为一个值-结果变量;
* cliaddr: 它将存放客户的协议地址
*/
int listenfd, connfd;
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
char buff[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
/*
* 下面情况都是在代码正常的情况下进行修改。
*
* 1. 注释掉 Listen(listenfd, LISTENQ);
* 错误提示为:
* 1)没有sudo权限时,提示:bind error: Permission denied。因为13端口是保留端口
* 2)给予权限时,提示:accept error: Invalid argument
*
* 2. 将端口号从13该为9999后,注释掉Listen(listenfd, LISTENQ);
* 错误提示:
* 1)accept error: Invalid argument。并没有出现权限的错误。
*
* 3. 删掉bind,保留listen
* 可以编译以及运行成功,但是不能正确的接收到请求。
*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for (;;) {
/*
* 我们将len初始化为套接字地址结构的大小,将指向cliaddr结构的指针和指向len的
* 指针分别作为accept的第二和第三个参数.调用inet_ntop将套接字地址结构中的32位
* IP地址转换为一个点分十进制ASCII字符串,调用ntohs将16位的端口号从网络字节序转
* 换为主机字节序
*/
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
#include
/*
* 返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
*
* 在子进程中可以通过getppid取得父进程的进程ID,则也就是为什么返回0的原因。
* 而在父进程中,可能有很多的子进程,所有返回子进程的ID
*
* 父进程中调用fork之前打开的所有描述符在fork返回之后有子进程共享。
*/
pid_t fork(void);
fork有两个典型用法:
# 典型的并发服务器轮廓
pid_t pid;
int listenfd, connfd;
listenfd = Socket( ... );
Bind(listenfd, ... );
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, ... );
if ((pid = Fork()) == 0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
父进程对connfd调用close没有终止它与客户的连接呢(先于子进程执行)?为了便于理解,我们必须知道每个文件或套接字都有一个引用计数。应用计数在文件表项中维护,它是当前打开着的引用该文件或套接字的描述符的个数。socket返回后与listenfd关联的文件表项的引用计数值为1。accept返回后与connfd关联的文件表项计数也为1。然而fork返回后,这两个描述符就在父进程与子进程间共享(也就是被复制),因此与这个两个套接字相关联的文件表项各自的访问计数值均为2。这么一来,当父进程关闭connfd时,它只是把相应的引用计数从2减为1。该套接字真正的清理和资源释放要等到其引用计数值到达0才发生。这会在稍后子进程也关闭connfd时发生。
#include
/*
* 返回:若成功则为0,若出错则为-1
*
* int close(int sockfd);
*/
int close(int sockfd);
如果引用计数值仍大于0,这个close调用并不引发TCP的四分组连接终止序列。但是我们确实想在某个TCP连接上发送一个FIN,那么可以改用shutdown函数代替close。
如果父进程对每个由accept返回的已连接套接字都不调用close,那么并发服务器中将会发生什么。首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻可拥有的打开着的描述符通常是有限制的。不过更重要的是,没有一个客户连接会被终止。当子进程关闭已连接套接字时,它的引用计数有2递减为1且保持为1,因为父进程永不关闭任何已连接套接字。这将妨碍TCP终止序列的发生,导致连接一直打开的。
这两个函数或者返回某个套接字关联的本地协议地址(getsockname),或者返回某个套接字关联的外地协议地址(getpeername)。
#include
/*
* 均返回:若成功则为0,若出错则为-1
*
* 这两个函数的最后一个参数都是值-结果。
*/
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
#include "unp.h"
int
sockfd_to_family(int sockfd) {
/*
* 既然不知道要分配的套接字地址结构的类型,我们于是采用sockaddr_storage这个
* 通用结构,因为它能够承载系统支持的任何套接字地址结构
*/
struct sockaddr_storage ss;
socklen_t len;
/*
* 我们调用getsockname返回地址族。既然POSIX规范运行对未绑定的套接字调用
* getsockname,该函数应该适合任何已打开的套接字描述符。
*/
len = sizeof(ss);
if (getsockname(sockfd, (SA *) &ss, &len) < 0)
return (-1);
return (ss.ss_family);
}