1. socket地址结构
sockaddr_in
ipv4 协议的地址结构是 sockaddr_in,ipv6 的地址结构是sockaddr_in6。
1 |
|
sin_family:表示地址簇,ipv4: AF_INET, ipv6: AF_INET6,sin_port:16位的端口号sin_addr:点分十进制。
通用地址结构
结构体是 sockaddr,方便可以接受 ipv4/ipv6 的地址结构。之所以采用 sockaddr,而不采用 void* 是因为 BSD 设计套接字的时候大约是 1982 年,那个时候的 C 语言还没有void * 的支持。
1 | struct sockaddr { |
将 sockaddr_in/sockaddr_in6 强制转换为 sockaddr ,通过 sockaddr 的 sa_family 来分别使用的是 ipv4/ipv6。
网络字节序列和主机字节序列转换
TCP/IP 协议规定,网络传输字节按照大端字节序列方式
- 大端:低地址存储在高位。
- 小端:低地址存低位。
sin_port 转换
1 |
|
sin_addr 转换
与协议无关的的转换函数,即 ipv4/ipv6都可以。
1 |
|
ipv4 专用的转换函数
1 |
|
2. socket 函数
socket
1 |
|
- 返回值
- 失败时返回
-1,成功是返回一个非零整数值,表示套接字描述符,sockfd。
- 失败时返回
- 参数
family:PF_INETPF_INET6PF_LOCAL
type:SOCK_STREAM: 表示的是字节流,对应TCP;SOCK_DGRAM: 表示的是数据报,对应UDP;SOCK_RAW: 表示的是原始套接字。- 还可和
SOCK_NOBLOCK和SOCK_CLOEXEC进行组合使用。
protocol:0,原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成。目前一般写成0即可。
- 状态:
- 创建
sockfd以后,处于CLOSED状态。
- 创建
connect
1 |
|
客户端调用函数。
- 返回值
- 成功返回
0,失败返回-1。 - 客户端调用
connect函数,会激发TCP的三次握手过程,而且仅仅在连接成功或者失败才返回。客户端是在第二个分节返回,服务端是第三个分节返回。 - *
ETIMEOUT*:若客户端没有收到SYN分节响应,就会返回这个错误。 - *
ECONNREFUSED*:若对客户端的SYN分节响应的是RST,表示服务器主机在指定的端口上没有进程与之连接,客户端一接受到RST就返回ECONNREFUSED错误。 - 不可达错误。
- 成功返回
- 参数
sockfd:是socket函数返回值。sockaddr:是套接字的地址结构,sockaddr_int/sockaddr_in6强制转换而来。addrlen:传入的地址结构大小。
- 状态:
connect会使得当前套接字从closed状态转移到SYN_SENT状态,如成功再转移到ESTABLISHED状态,若失败则该套接字不可用,必须关闭。
bind
1 |
|
服务器端调用函数。
- 参数
addr- 可以使用
通配地址对于IPv4的地址来说,使用INADDR_ANY来完成通配地址的设置;对于IPv6的地址来说,使用IN6ADDR_ANY来完成通配地址的设置 0, 系统随机分配
- 可以使用
port- 绑定端口,一般选择大于
1024的端口。 0, 系统随机分配
- 绑定端口,一般选择大于
1 | struct sockaddr_in name; |
listen
1 |
|
listen 函数由服务器端调用。
初始化创建的套接字,可以认为是一个”主动”套接字,其目的是之后主动发起请求(通过调用 connect 函数)。通过 listen 函数,可以将原来的”主动”套接字转换为”被动”套接字,告诉操作系统内核:”我这个套接字是用来等待用户请求的。”, 操作系统内核会为此做好接收用户请求的一切准备,比如完成连接队列。
返回值
- 失败时返回
-1,成功返回0。
- 失败时返回
参数
sockfd- 初始化套接字
backlog内核为相应套接字排队的最大连接个数,这个参数的大小决定了可以接收的并发数目。
内核为每个监听套接字维护两个队列:
(1) 半连接队列(SYN队列):接收到一个SYN建立连接请求,处于SYN_RCVD状态;
(2) 全连接队列(Accept队列):已完成TCP三次握手过程,处于ESTABLISHED状态;在早期
Linux内核backlog是SYN队列大小,也就是未完成的队列大小。在Linux内核2.2之后,SYN队列由/proc/sys/net/ipv4/tcp_max_syn_backlog指定;backlog变成Accept队列,也就是已完成连接建立的队列长度,所以现在通常认为backlog是accept队列。但是上限值是内核参数somaxconn的大小,也就说 **Accept队列长度 =min(backlog, somaxconn)**。
状态转移
- 当来自客户的
SYN分节到达时,TCP在未连接队列创建一个新项,然后响应以三次握手的第二个分节,这一项一直保留在未完成连接队列中,直到三次握手的第三个分节到达或者超时。如果到达,该项就从未完成连接队列中移到已完成连接队列的队尾。当调用accept时,已完成连接队列的队首将作为accept的返回值,如果已完成连接队列是空,那么调用accept函数的进程会进入睡眠状态。
- 当来自客户的
accept
1 |
|
调用 accept 时,已完成连接队列的队首将作为 accept 的返回值,如果已完成连接队列是空,那么调用进程进入睡眠状态。成功返回客户端的已连接套接字connfd,失败返回 -1。
accpet 函数返回时,表示已连接套接字 connfd 和服务器端的监听套接字 listenfd 完成了三次握手。
EMFILE
如果函数accept返回EMFILE,即文件描述符过多,怎么处理?先实现准备一个空闲的文件描述符 *
/dev/null*。遇到这种情况,先关闭这个空闲的文件描述符,就可以获得一个文件描述名额,然后再accept就可以拿到这个连接的socket文件描述符,随后立即close,就优雅的断开了与客户端的连接,最后重新打开空闲文件,以备这种情况再次出现。
close
1 |
|
这个函数表示的把该套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再被调用进程使用。
- 注意事项
- 由于描述符是引用计数,
close只是减少该引用计数,只有当该引用计数为0时才会引用终止序列。 - tcp会先将已经排队等待发送到对端的任何数据发送过去,然后再发送终止序列
FIN。因此,调用close不是立即发送终止序列。
- 由于描述符是引用计数,
shutdown
1 |
|
shutdown 解决的是 close 的两个限制:
close把描述符计数减一,仅仅在计数变为0时才关闭套接字。shutdown可以不管描述符计数就激发TCP的正常连接终止序列。close终止读和写两个方向的数据传递,shutdown是半关闭,可以只是关闭一个方向数据流。参数
how:SHUT_RD:关闭读SHUT_WR:关闭写SHUT_RDWR:关闭读写