OSI模型
在这里不详细展开tcp/ip计算机网络相关知识,可能会专门写一篇这方面文章吧。
详细了解可看程皓的博客文章:TCP的那些事(上)与TCP的那些事(下)。
传输层:TCP/UDP原理简介
用户数据报协议(UDP)
UDP提供无连接的服务,是一个简单的传输协议,应用程序往一个UDP套接字写入一个消息,该消息随后被封装到一个UDP数据报,该UDP数据报进而又被封装到一个IP数据报,然后发往目的地。
UDP不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证 每个数据报只到达一次。
传输控制协议(TCP)
TCP提供有连接服务,先建立连接,在交换数据,最终关闭连接。
TCP还提供可靠性(reliability)。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间。在数次重传失败后,TCP才放弃。
TCP含有用于动态估算客户和服务器之间的往返时间(round-trip time, RTT)的算法,以便它知道等待一个确认需要多少时间。
TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序。
TCP提供流量控制(flow control)。总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window)。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。该窗口时刻变化。
tcp连接是全双工的(full-duplex)。在一个给定的连接上应用可以在任何时刻在进出两个方向上即发送数据又接收数据。
三次握手
建立一个TCP连接:
- 服务器必须准备好接受外来的连接。通常通过调用
socket
,bind
和listen
这三个函数完成,称之为被动打开(passive open)。 - 客户端通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分组不携带数据,其所在IP数据报只含有一个IP首部,一个TCP首部及可能有的TCP选项。
- 服务端必须确认(ACK)客户端的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一个连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
- 客户端必须确认服务器的SYN。
TCP选项
- MSS选项(最大分节大小 maximum segment size)。即在本连接的每个TCP分节中愿意接受的最大数据量,发送端TCP使用接收端的MSS值作为所发送分节的最大大小。
- 窗口规模选项。
- 时间戳选项。可以防止由失而复现的分组可能造成的数据损坏。
关闭连接
TCP建立一个连接需要三个分节,终止一个连接需要四个分节:
- 某个应用进程首先调用close,称该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
- 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
- 一段时间后,接收到这个文件结束符的应用进程将调用
close
关闭它的套接字。这导致它的TCP也发送一个FIN。 - 接收到这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
某些情况下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。
在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为半关闭(half-close)。
状态转换
TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。
当某个应用进程在CLOSE状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着收到一个带ACK的SYN,它将发送一个ACK,且新状态是ESTABLISHED。
如果某个应用进程在接收到一个FIN之前调用close主动关闭,那么就转换到FIN_WAIT_1状态。
如果某个应用进程在ESTABLISHED状态期间收到一个FIN被动关闭,那就转换到CLOSE_WAIT状态。
TIME_WAIT状态
执行主动关闭那端进入了TIME_WAIT状态,该端停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime, MSL)的两倍,有时称为2MSL。 MSL是任何IP数据报能够在因特网中存活的最长时间。
TIME_WAIT状态存在的两个理由:
- 可靠地实现TCP全双工连接的终止。
- 允许老的重复分节在网络中消逝。
套接字编程基础
套接字地址结构
IPV4套接字地址结构
1 | struct in_addr { |
2 | in_addr_t s_addr; /* 32-bit IPv4 address */ |
3 | /* network byte ordered */ |
4 | }; |
5 | |
6 | struct sockaddr_in { |
7 | uint8_t sin_len; /* length of structure (16) */ |
8 | sa_family_t sin_family; /* AF_INET */ |
9 | in_port_t sin_port; /* 16-bit TCP or UDP port number */ |
10 | /* network byte ordered */ |
11 | struct in_addr sin_addr; /* 32-bit IPv4 address */ |
12 | /* network byte ordered */ |
13 | char sin_zero[8]; /* unused */ |
14 | }; |
通用套接字地址结构
套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一。
1 | struct sockaddr { |
2 | uint8_t sa_len; |
3 | sa_family_t sa_family; /* address family: AF_xxx value */ |
4 | char sa_data[14]; /* protocol-specific address */ |
5 | }; |
1 | int bind(int, struct sockaddr *, socklen_t); |
1 | struct sockaddr_in serv; /* IPv4 socket address structure */ |
2 | |
3 | /* fill in serv{} */ |
4 | |
5 | bind(sockfd, (struct sockaddr *) &serv, sizeof(serv)); |
IPV6套接字地址结构
1 | struct in6_addr { |
2 | uint8_t s6_addr[16]; /* 128-bit IPv6 address */ |
3 | /* network byte ordered */ |
4 | }; |
5 | |
6 |
|
7 | |
8 | struct sockaddr_in6 { |
9 | uint8_t sin6_len; /* length of this struct (28) */ |
10 | sa_family_t sin6_family; /* AF_INET6 */ |
11 | in_port_t sin6_port; /* transport layer port# */ |
12 | /* network byte ordered */ |
13 | uint32_t sin6_flowinfo; /* flow information, undefined */ |
14 | struct in6_addr sin6_addr; /* IPv6 address */ |
15 | /* network byte ordered */ |
16 | uint32_t sin6_scope_id; /* set of interfaces for a scope */ |
17 | }; |
下图为5种常见的套接字:IPv4,IPv6,Unix域,数据链路与存储的比较。
字节排序(大小端)
小端模式(little-endian):将低序列字节存储在起始地址。
大端模式(big-endian):将高序字节存储在起始地址。
如下图所示:最高有效位(most significant bit, MSB),最低有效位(least significant bit, LSB)。
这两种字节序都有系统使用,某个给定系统所使用的字节序又被称为:主机字节序(host byte order)。
1 |
|
2 | |
3 | int main(int argc, char **argv) |
4 | { |
5 | union { |
6 | short s; |
7 | char c[sizeof(short)]; |
8 | } un; |
9 | |
10 | un.s = 0x0102; |
11 | printf("%s: ", CPU_VENDOR_OS); |
12 | if (sizeof(short) == 2) { |
13 | if (un.c[0] == 1 && un.c[1] == 2) |
14 | printf("big-endian\n"); |
15 | else if (un.c[0] == 2 && un.c[1] == 1) |
16 | printf("little-endian\n"); |
17 | else |
18 | printf("unknown\n"); |
19 | } else |
20 | printf("sizeof(short) = %d\n", sizeof(short)); |
21 | |
22 | exit(0); |
23 | } |
主机序与网络序转换函数。
1 |
|
2 | |
3 | uint16_t htons(uint16_t host16bitvalue); |
4 | uint32_t htonl(uint32_t host32bitvalue); |
5 | uint16_t ntohs(uint16_t net16bitvalue); |
6 | uint32_t ntohl(uint32_t net32bitvalue); |
字节操作函数
1 |
|
2 | |
3 | void bzero(void *dest, size_t nbytes); |
4 | void bcopy(const void *src, void *dest, size_t nbytes); |
5 | int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); |
1 |
|
2 | |
3 | void *memset(void *dest, int c, size_t len); |
4 | void *memcpy(void *dest, const void *src, size_t nbytes); |
5 | int memcmp(const void *ptr1, const void *ptr2, size_t nbytes); |
1 |
|
2 | |
3 | int inet_aton(const char *strptr, struct in_addr *addrptr); |
4 | in_addr_t inet_addr(const char *strptr); |
5 | char *inet_ntoa(struct in_addr inaddr); |
TCP套接字编程
socket函数
为了执行网络I/O,一个进程第一件事是调用socket
函数,指定期望的通信协议类型。
1 |
|
2 | int socket (int family, int type, int protocol); |
3 | //成功返回非负描述符,出错则为-1。 |
family参数指明协议族(协议域),type参数指明套接字类型,protocol参数应设为某个协议类型常值,或者设为0,以选择所给定family和type组合的系统默认值。
family | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 秘钥套接字 |
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
并非所有套接字family与type的组合都是有效的,下图给出一些有效的组合和对应的真正协议。
family和type参数的组合 | AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|---|---|---|---|---|
SOCK_STREAM | TCP/SCTP | TCP/SCTP | 是 | ||
SOCK_DGRAM | UDP | UDP | 是 | ||
SOCK_SEQPACKET | SCTP | SCTP | 是 | ||
SOCK_RAW | IPv4 | IPv6 | 是 | 是 |
connect函数
tcp客户端使用connect函数来建立与tcp服务器的连接。客户端在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源地址,并选择一个临时端口作为源端口。
1 |
|
2 | int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); |
如果是TCP套接字,调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:
- 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。
- 若对客户的SYN响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也行没有在运行),这是一种硬错误,客户端收到RST就返回ECONNREFUSED错误。
- 若客户发出的SYN在中间的某个路由器上引发一个”destination unreachable”(目的地不可达)ICMP错误,返回EHOSTUNREACH或ENETUNREACH错误。
bind函数
bind函数把一个本地协议地址赋予一个套接字。
1 |
|
2 | int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); |
对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,或者都不指定。
ip地址 | 端口 | 结果 |
---|---|---|
通配地址 | 0 | 内核选择IP地址和端口 |
通配地址 | 非0 | 内核选择IP,进程指定端口 |
本地IP地址 | 0 | 进程指定IP地址,内核选择端口 |
本地IP地址 | 非0 | 进程指定IP和端口 |
对于IPv4来说,通配地址由常值INADDR_ANY来指定。
1 | struct sockaddr_in servaddr; |
2 | servaddr.sin_addr.s_addr = htonl(INADDR_ANY); |
listen函数
listen函数仅由TCP服务器调用,它做两件事:
- 当socket函数创建一个套接字时,它被假设为一个主动套接字,即将调用connect发起连接的客户端套接字,listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
- listen函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
1 |
|
2 |
|
内核为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态。
- 已完成连接队列,每个已完成TCP三次握手过程的客户端对应其中一项,这些套接字处于ESTABLISHED状态。
两个队列之和不超过backlog。
当客户端的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三次握手的第二个分节:服务器的SYN相应,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,直到三次握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。如果三次握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的对头将返回给进程。
未完成连接队列中的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器。
accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回一个已完成连接。如果已完成连接队列为空,进程投入睡眠。
1 |
|
2 | |
3 | int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); |
1 |
|
2 |
|
3 | |
4 | int main(int argc, char **argv) |
5 | { |
6 | int listenfd, connfd; |
7 | socklen_t len; |
8 | struct sockaddr_in servaddr, cliaddr; |
9 | char buff[MAXLINE]; |
10 | time_t ticks; |
11 | |
12 | listenfd = Socket(AF_INET, SOCK_STREAM, 0); |
13 | |
14 | bzero(&servaddr, sizeof(servaddr)); |
15 | servaddr.sin_family = AF_INET; |
16 | servaddr.sin_addr.s_addr = htonl(INADDR_ANY); |
17 | servaddr.sin_port = htons(13); /* daytime server */ |
18 | |
19 | bnind(listenfd, (SA *) &servaddr, sizeof(servaddr)); |
20 | |
21 | listen(listenfd, LISTENQ); |
22 | |
23 | for ( ; ; ) { |
24 | len = sizeof(cliaddr); |
25 | connfd = Accept(listenfd, (SA *) &cliaddr, &len); |
26 | printf("connection from %s, port %d\n", |
27 | Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), |
28 | ntohs(cliaddr.sin_port)); |
29 | |
30 | ticks = time(NULL); |
31 | snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); |
32 | write(connfd, buff, strlen(buff)); |
33 | |
34 | close(connfd); |
35 | } |
36 | } |
close函数
close函数也用来关闭套接字,并终止tcp连接。
1 |
|
2 | int close (int sockfd); |
getsockname和getpeername函数
这两个函数或者返回与某个套接字关联的本地协议地址或者返回与某个套接字关联的外地协议地址。
1 |
|
2 | |
3 | int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen); |
4 | int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); |
套接字选项
1 |
|
2 | |
3 | int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); |
4 | int setsockopt(int sockfd, int level, int optname, const void *optval socklen_t optlen); |