socket编程


socket是进程通讯的一种方式,即调用这个网络库的一些API函数实现分布在不同主机的相关进程之间的数据交换。在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器(Client/Server, C/S)模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。

用户数据报协议(UDP)与 传输控制协议(TCP)

UDP是一个简单、不可靠、无连接的协议,而TCP是一个复杂、可靠、面向连接的协议。

用户数据报协议(UDP):

UDP不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次。

传输控制协议(TCP):

  1. TCP提供可靠性。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间。在数次重传失败后,TCP才放弃。
  2. TCP含有用于动态估算客户和服务器之间的往返时间(round-trip time, RTT)的算法,以便它知道等待一个确认需要多少时间。
  3. TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序。
  4. TCP提供流量控制。TCP总是告知对端在任何时刻它一次能够从对端接收多少个字节,这称为滑动窗口。

TCP三路握手协议

建立一个TCP连接会发生下述情形,如图:

handshake.png

  1. 服务器必须准备好接收外来的连接。这通常通过调用socketbind、和listen这3个函数来完成,我们称之为被动打开。服务器的状态由 CLOSED 转换为 LISTEN
  2. 客户通过调用connect发起主动打开。这导致客户TCP发送一个 SYN (同步)分节,它告诉服务器客户所在(待建立的)连接中发送的数据的初始序列号。通常 SYN 分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。客户端的状态转换为 SYN_SENT
  3. 服务器必须确认( ACK )客户的 SYN ,同时自己也得发送一个 SYN 分节,它含有服务器在同一连接中发送的数据非初始序列号。服务器在单个分节中发送 SYN 和对客户 SYNACK (确认)。服务器的状态转换为SYN_RECV
  4. 客户必须确认服务器的 SYN。客户端的状态转换为ESTABLISHED

TCP连接终止

TCP建立一个连接需要3个分节,终止一个连接需要4个分节。如图:

close.png

  1. 某个应用进程首先调用close,我们称该端执行主动关闭。该端的TCP于是发送一个 FIN 分节,表示数据发送完毕。客户端的状态转换为 FIN_WAIT_1
  2. 接收到这个 FIN 的对端执行被动关闭。这个 FIN 由TCP确认。它的接收也是一个文件结束符。传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为 FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。服务器的状态转换为 CLOSE_WAIT
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个 FIN。服务器的状态转换为 LAST_ACK
  4. 接收这个最终 FIN 的原发送端TCP(即执行主动关闭的那一端)确认这个 FIN。客户端状态转换为 TIME_WAIT

既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。在某些情形下步骤1的 FIN 随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。这称为 半关闭

当套接字被关闭时,其所在端TCP各自发送了一个FIN。这是由应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

上图中展示了客户端执行主动关闭的情形,不过,无论是客户端还是服务器,任何一端都可以执行主动关闭。通常情况是客户执行主动关闭,但是某些协议(譬如HTTP),却由服务器执行主动关闭。

TIME_WAIT状态

TIME_WAIT状态是执行主动关闭的那段经历的状态。该端点停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime, MSL)的两倍,有时候称之为 2MSL

MSL 是任何IP数据报能够在因特网中存活的最长时间。这个时间是有限的,因为每个数据包含有一个称为跳限的8位字段(TTL 字段),它的最大值为255。尽管这是一个跳数限制而不是真正的时间限制,我们仍然假设:具有最大跳限(255)的分组在网络中存在的时间不可能超过 MSL 秒。

分组在网络中“ 迷途 ” 通常是路由异常的结果。某个路由器崩溃或某两个路由器之间的某个链路断开时,路由协议需花数秒钟到数分钟的时间才能稳定并找出另一条通路。在这段时间有可能发生路由循环(路由器A把分组发送给路由器B,而B再把它们发送回A),分组可能就此陷入这样的循环。假设迷途的分组是一个TCP分节,在他迷途期间,TCP超时并重传该分组,而重传的分组却通过某条路径到达最终的目的地。然而不久后(自迷途的分组在MSL秒以内)路由循环修复,这个被迷失的分组也最终被送到目的地。TCP必须正确的处理这些重复的分组。

TIME_WAIT状态有两个存在的理由

  • 可靠地实现TCP全双工连接的终止
  • 运行老的重复分节在网络中消逝

假设客户端关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身,因为他们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再出现,为做到这一点,TCP将不给处于 TIME_WAIT 状态的连接发起新的连接。既然 TIME_WAIT 状态的持续时间是 MSL 的2倍,这足以让某个方向上的分组最多存活 MSL 秒即被丢弃,另一个方向上的应答最多存活 MSL 秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。

套接字地址结构

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议栈都定义它自己的套接字地址结构。这些结构的名字以sockaddr_开头,并以对应每个协议栈的唯一后缀结尾。

IPV4套接字地址结构

IPV4套接字地址结构通常也称为”网际套接字地址结构”,它以sockaddr_in命名,定义在<netinet/in.h>头文件中。它的POSIX定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct in_addr {
in_addr_t s_addr /* 32-bit IPV4 address */
/* network byte ordered */
};

struct sockaddr_in {
uint8_t sin_len; /* length of structure */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit IPV4 address */
/* network byte ordered */
};
  • POSIX规范只需要这个结构中的3个字段:sin_family、sin_addr和sin_port。对于符合POSIX的实现来说,定义额外的结构字段是可以接受的,这对于网际套接字地址结构来说是正常的。几乎所有的实现都增加了sin_zero字段,所以所有的套接字地址结构大小都至少是16字节。
  • in_addr_t数据类型必须是一个至少32位的无符号整数类型,in_port_t必须是一个至少16位的无符号整数类型,而sa_family_t可以是任何无符号整数类型。在支持长度字段的实现中,sa_family_t通常是一个8位的无符号整数,而在不支持长度字段的实现中,它则是一个16位的无符号整数。
  • IPv4地址和TCP或UDP端口号在套接字地址结构中总是以网络字节序来存储。
  • 32位IPv4地址存在两种不同的访问方法。举例来说,如果serv定义为某个网际套接字地址结构,那么serv.sin_addr将按in_addr结构引用其中的32位IPv4地址,而serv.sin_addr.s_addr将按in_addr_t(通常是一个无符号的32位整数)引用同一个32位IPv4地址。因此,我们必须正确地使用IPv4地址,尤其是在将它作为函数的参数时,因为编译器对传递结构和传递整数的处理是完全不同的。
  • sin_zero字段未曾使用,不过在填写这种套接字地址结构时,我们总是把该字段置为0。按照惯例,我们总是在填写前把整个结构置为0,而不是单单把sin_zero字段置为0。

通用套接字地址结构

当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是该结构的指针)来传递。然而以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构。

<sys/socket.h>头文件中定义一个通用的套接字地址结构

1
2
3
4
5
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};

于是套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一,这正如bind函数的ANSI C函数原型所示:

1
int bind(int, struct sockaddr *, socklen_t)

这就要求对这些函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行类型转换,变成指向某个通用套接字地址结构的指针,例如:

1
2
struct sockaddr_in serv;
bind(sockfd, (struct sockaddr *)&serv, sizeof(serv))

字节序

考虑一个16位整数,它由2个字节组成。内存中存储这两个字节有两种方法:一个是将低序字节存储在起始地址,这称为小端(little-endian)字节序;另一种方法是将高序字节存储在起始地址,这称为大端(big-endian)字节序。

判断一个系统的字节序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(int argc, char *argv[]) {
union {
short s;
char c[sizeof(short)];
} un;

un.s = 0x0102;

if(sizeof(short) == 2) {
if(un.c[0] == 0x01 && un.c[1] == 0x02) {
printf("big-endian\n");
} else if(un.c[0] == 0x02 && un.c[1] == 0x01) {
printf("little-endian\n");
} else {
printf("unknow\n");
}
} else {
printf("sizeof(short) = %d\n", sizeof(short));
}

return 0;
}

另一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char *argv[]) {
short s = 0x0102;
char *p = (char *)&s;

if(sizeof(short) == 2) {
if(p[0] == 0x01 && p[1] == 0x02) {
printf("big-endian\n");
} else if(p[0] == 0x02 && p[1] == 0x01) {
printf("little-endian\n");
} else {
printf("unknow\n");
}
} else {
printf("sizeof(short) = %d\n", sizeof(short));
}

return 0;
}

字节序之间的转换的4个函数

1
2
3
4
5
6
7
8
#include <netinet/in.h>

uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
均返回:网络字节序值
uint16_t ntohs(uint16_t host16bitvalue);
uint32_t ntohl(uint32_t host32bitvalue);
均返回:主机字节的值

在这些函数的名字中,h代表host,n代表network,s代表short,l代表long。应该把s视为一个16位的值(例如TCP或UDP端口号),把l视为32的值(例如IPV4地址)。尽管长整数占用64位,htonlntohl函数操作的仍然是32位的值。

当使用这些函数时,我们并不关心主机字节序和网络字节序的真实值(或为大端,或为小端)。我们所要做的只是调用适当的函数在主机和网络字节序之间转换某个给定值。在那些与网际协议所用字节序(大端)相同的系统中,这四个函数通常被定义为空宏。

inet_aton inet_addr 和 inet_ntoa函数

1
2
3
4
5
6
7
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr)
返回: 若字符串有效则为1,否则为0
in_addr_t inet_addr(const char *strptr);
返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE
char *inet_ntoa(struct in_addr inaddr);
返回:指向一个点分十进制串的指针

inet_aton将strptr所指C字符串转换成一个32位的网络字节二进制值,并通过指针addrptr来存储。若成功则返回1,否则返回0。

inet_addr已被废弃,新的代码应该改用inet_aton函数,更好的是使用inet_pton函数,它们对于IPV4地址和IPv6地址都适用。

inet_ntoa函数将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。这意味着该函数是不可重入的。

inet_pton 和 inet_ntop函数

这两个函数是随IPv6出现的新函数,对于IPv4地址和IPv6地址都适用。

1
2
3
4
5
6
#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);
返回:若成功则为1,若输入不是有效的表达格式则为0,若出错则为-1
const char *inet_ntop(int family, const void* addrptr, char *strptr, size_t len);
返回:若成功则为指向结果的指针,若出错则为NULL

这两个函数的family参数既可以是AF_INET,也可以是AF_INET6。如果以不被支持的地址族为family参数,这两个函数就都返回一个错误,并将errno置为EAFNOSUPPORT。

第一个函数尝试转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果。若成功则返回值为1,否则如果对所指定的family而言输入的字符串不是有效的表达格式,那么返回值为0。

inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达格式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。为了有助于指定这个大小,在<netinet/in.h>头文件中有如下定义:

1
2
#define INET_ADDRSTRLEN  16  
#define INET6_ADDRSTRLEN 46

如果len大小,不足以容纳表达式结果(包括结尾的空字符),那么返回一个空指针,并置errno为ENOSPC。

inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

TCP套接字编程

一个完整的TCP客户/服务器程序所需要的基本套接字函数,如图:

socket.png

TCP时间获取的客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>                                                        
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>

#include "error.h"

#define MAXLINE 4096
#define SERV_PORT 9987
#define SA struct sockaddr

int main(int argc, char *argv[]) {
struct sockaddr_in servaddr;
int sockfd;
char buf[MAXLINE];
int n;

if(argc != 2) {
err_quit("usage: %s <IPAddress>", argv[0]);
}

if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
err_sys("socket error");
}

bzero(&servaddr, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
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, buf, MAXLINE - 1)) > 0) {
buf[n] = '\0';

if(fputs(buf, stdout) == EOF) {
err_sys("fputs error");
}
}

if(n < 0) {
err_sys("read error");
}

return 0;
}

TCP时间获取的服务器程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <stdio.h>                                                        
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>

#include "error.h"

#define MAXLINE 4096
#define SERV_PORT 9987
#define SA struct sockaddr
#define LISTENQ 1024

static int Socket(int family, int type, int protocol) {
int n;

if((n = socket(family, type, protocol)) < 0) {
err_sys("socket error");
}

return n;
}

static void Bind(int fd, struct sockaddr *sa, socklen_t salen) {
if(bind(fd, sa, salen) < 0) {
err_sys("bind error");
}
}

static void Listen(int fd, int backlog) {
char *ptr;

if((ptr = getenv("LISTENQ")) != NULL) {
backlog = atoi(ptr);
}

if(listen(fd, backlog) < 0) {
err_sys("listen error");
}
}

static int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {
int n;

while((n = accept(fd, sa, salenptr)) < 0) {
if(errno != EINTR && errno != ECONNABORTED) {
err_sys("accept error");
}
}

return n;
}

static void Write(int fd, void *vptr, size_t nbytes) {
if(write(fd, vptr, nbytes) != nbytes) {
err_sys("write error");
}
}

static void Close(int fd) {
if(close(fd) == -1) {
err_sys("close error");
}
}

int main(int argc, char *argv[]) {
struct sockaddr_in servaddr;
int listenfd;
int connfd;
time_t ticks;
char buf[MAXLINE];

listenfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

Listen(listenfd, LISTENQ);

for(;;) {
connfd = Accept(listenfd, NULL, NULL);

ticks = time(NULL);
snprintf(buf, MAXLINE, "%.24s\n", ctime(&ticks));
Write(connfd, buf, strlen(buf));

Close(connfd);
}

return 0;
}

运行服务器程序:

[heql@ubuntu socket]$ ./daytimetcpserv 

运行客户端程序,输入如下:

[heql@ubuntu socket]$ ./daytimetcpcli 127.0.0.1
Fri Dec 15 11:37:36 2017

socket函数

为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型。

1
2
3
#include <sys/socket.h>
int socket(int family, int type, int protocol);
返回:若成功则为非负描述符,若出错则为-1

其中family参数指明协议栈,该参数也往往被称为协议域。type参数指明套接字类型(SOCK_STREAM :字节流套接字, SOCK_DGRAM :数据报套接字),protocol参数应设为某个协议类型的常值,或者设为0,以选择所给定familytype组合的系统默认值。

connect函数

TCP客户用connect函数来建立与TCP服务器的连接。

1
2
3
#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错误,则认为是一种软错误。客户内核保存该消息,并按第一种情况中所述的时间间隔继续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACHENETUNREACH错误返回给进程。以下两种情形也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;而是connect调用根本不等待就返回。

指定本地子网(192.168.1/24)上其主机ID(100)并不存在的一个IP地址,也就是说本地子网没有一个主机ID为100的主机,这样当客户主机发出ARP请求(要求那个不存在的主机响应以其硬件地址)时,它将永远收不到ARP响应:

[heql@ubuntu socket]$ ./daytimetcpcli 192.168.1.100
connect error: Connection timed out

我们指定一个没有运行时间获取服务器程序的主机(其实是一个本地路由器)。

[heql@ubuntu socket]$ ./daytimetcpcli 127.0.0.1
connect error: Connection refused

服务器主机立刻响应一个RST分节。

指定一个因特网不可到达的IP地址,路由器返回了主机不可达的ICMP错误。

[heql@ubuntu socket]$ ./daytimetcpcli 192.3.4.5
connect error: No route to host

connect函数导致当前套接字从 CLOSED 状态(该套接字自从由socket函数创建以来一直所处的状态)转移到 SYN_SENT 状态,若成功则再转移到 ESTABLISHED 状态。若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。当循环调用函数connect为给定主机尝试各个IP地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket

bind函数

bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。

1
2
3
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
返回:若成功则为0,若出错则为-1

第二个参数是指一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

  • 服务器在启动时捆绑它们的众所周知端口,如果一个TCP客户或服务器未曾调用bind捆绑一个端口,当调用connectlisten时,内核就要为相应的套接字选择一个临时端口。让内核来选择临时端口对于TCP客户来说是正常的,除非应用需要一个预留端口;然而对于TCP服务器来说却极为罕见,因为服务器是通过它们的众所周知端口被大家认识的。

这个规则的例外是远程调用(Remote Procedure Call, RPC)服务器。它们通常就由内核为它们的监听套接字选择一个临时端口,而该端口随后通过RPC端口映射器进行注册。客户在connect这些服务器之前,必须与端口映射器联系以获取它们的临时端口。这种情况也适用于使用UDP的RPC服务器

  • 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户,这就为在该套接字上发送的IP数据报指派了源IP地址。对于TCP服务器,这就限定该套接字只接收那些目的地为这个IP地址的客户连接。TCP客户通常不把IP地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径。如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户发送的 SYN 的目的IP地址作为服务器的源IP地址。

如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。

对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0。它告知内核去选择IP地址。

listen函数

listen函数仅由TCP服务器调用,它做两件事情。

  1. socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen导致套接字从 CLOSED 状态转换到LISTEN 状态。
  2. 本函数的第二个参数规定了内核应该为相应套接字已连接队列里排队的最大连接个数。
1
2
3
#include <sys/socket.h>
int listen(int sockfd, int backlog);
返回:若成功则为0,若出错则为-1

本函数通常应该在调用socketbind这两个函数之后,并在调用accept函数之前调用。

为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:

  • 未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于 SYN_RCVD 状态。
  • 已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于 ESTABLISHED 状态。

每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。如图:

listen.png

当来自客户的 SYN 到达时,TCP在未完成连接队列中创建一个新项,然后响应已三路握手的第二个分节:服务器的 SYN 响应,其中捎带对客户 SYNACK。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器 SYNACK)到达或者该项超时为止。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的对尾。当进程调用accept时,已完成连接队列中的对头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

应用进程应该指定多大值的backlog呢?当今的HTTP服务器指定了一个较大的值,但是如果这个指定值在源代码中是一个常值,那么增大其大小需要重新编译服务器程序。另一个方法是设定一个默认值,不过允许通过命令行选项或 环境变量 覆写该默认值。指定一个比内核支持的值还要大的backlog也是可接受的,因为内核应该悄然把所指定的偏大值截成自身支持的最大值,而不返回错误。

1
2
3
4
5
6
7
8
9
10
11
void Listen(int fd, int backlog) {
char *ptr;

if((ptr = getenv("LISTENQ")) != NULL) {
backlog = atoi(ptr);
}

if(listen(fd, backlog) < 0) {
err_sys("listen error");
}
}

当一个客户 SYN 到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送 RST 。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在这些队列中找到可用空间。要是服务器TCP立即响应以一个 RST ,客户connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区分响应 SYNRST 究竟意味着“该端口没有服务器在监听”,还是意味着“该端口由服务器在监听,不过它的队列满了”。

在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接该套接字的接收缓冲区大小。

accept函数

accept函数由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。

1
2
3
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回:若成功则为非负描述符,若出错则为-1

参数cliaddraddrlen用来返回已连接的对端进程(客户)的协议地址。addrlen是值-结果参数:调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放该套接字地址结构内的确切字节数。如果我们对返回协议地址不感兴趣,那么可以把cliaddraddrlen均值为空指针。

如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的连接。在讨论accept函数时,我们称它的第一个参数为监听套接字描述符,称accept的返回值为已连接套接字描述符。区分这两个套接字非常重要。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

修改上面的代码以显示客户端的IP地址和端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static const char *Inet_ntop(int family, void *src,     char *dest, socklen_t size) {
const char *ptr;

if((ptr = inet_ntop(family, src, dest, size)) == NULL) {
err_quit("inet_ntop error for %s", src);
}

return ptr;
}

int main(int argc, char *argv[]) {
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t clilen;
int listenfd;
int connfd;
time_t ticks;
char buf[MAXLINE];

listenfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

Listen(listenfd, LISTENQ);

for(;;) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buf, MAXLINE),
ntohs(cliaddr.sin_port));

ticks = time(NULL);
snprintf(buf, MAXLINE, "%.24s\n", ctime(&ticks));
Write(connfd, buf, strlen(buf));

Close(connfd);
}

return 0;
}

运行服务器程序:

[heql@ubuntu socket]$ ./daytimetcpserv

运行客户端程序,输入如下:

[heql@ubuntu socket]$ ./daytimetcpcli 192.168.1.156
Fri Dec 15 16:35:07 2017

服务器输出:

[heql@ubuntu socket]$ ./daytimetcpserv 
connection from 192.168.1.156, port 58302

close函数

close函数也用来关闭套接字,并终止TCP连接。

#include <unsitd.h>
int close(sockfd);
                    返回:若成功则为0,若出错则为-1

close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为readwrite的第一参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发送的是正常的TCP连接终止序列。