套接字地址结构
ipv4
套接字地址结构
POSIX定义如下:
struct in_addr {
in_addr_t s_addr; /* 32bit 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 */
char sin_zero[8];/* unused */
}
sin_len字段,是由处理来自不同协议族的套接字地址结构的例程(例如路由表处理代码)在内核中使用的,无须设置和检查它;
sin_family sa_port sa_addr三个字段是posix规范的,其他字段也是可以接受的,sa_port 与sa_addr要求是网络字节序;
-
ipv4地址使用:存在两种不同访问方法,1)
serv.sin_addr
其类型为in_addr
结构体;2)serv.sin_addr.s_addr
其类型为in_addr_t
(通常为无符号32位整数);历史原因struct in_addr结构中只存在一个成员。因之前作为联合使用,后面废弃;
通用套接字地址结构
在
头文件定义如下:
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; /* address family: AF_xxx value */
char sa_data[14]; /* protocol-specific address */
}
其中bind()
函数入参使用的就是该结构定义:
int bind(int, struct sockaddr *, socklen_t);
因此,使用ipv4
套接字地址结构需要强制类型转换;
主机字节序
内存存储多字节数据由于不同系统存储方式不同,将某个给定系统所用的字节序叫做主机字节序
,分为两种模式:
-
小端字节序
低序字节在低地址,高序字节在高地址;
-
大端字节序
与小端相反,低序字节在高地址,高序字节在低地址;
网络字节序
网络协议必须指定一个网络字节序
,网际协议使用大端字节序
来传送多字节整数(如16位端口及32位的ipv4地址),并且套接字地址结构中的端口及地址必须按照网络字节序来维护,因此,需要关注如何在主机字节序和网络字节序之间的转换,
提供了转换函数:
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);
h
代表主机字节序,n
代表网络字节序,s
代表short
,l
代表long
;
地址转换函数
将ASCII字符串(偏爱使用的格式)与网络字节序的二进制值 之间转换网际地址;
#include
//适用ipv4
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
char *inet_ntoa(struct in_addr inaddr);
//适用ipv4 ipv6
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//family可以为AF_INET或者为AF_IENT6
其中inet_addr
已废弃,使用inet_aton
;
基本套接字编程
socket()
套接字描述符如文件描述符一样,存在引用计数,如fork被子进程复制后引用计数+1,只有引用计数为0时,内核才会关闭该描述符;
connect()
connect函数前为啥不需要bind绑定地址,因为内核会根据外出网络接口确定源ip地址(而所用网络接口则取决于到达服务器所需的路径),并选择一个临时端口作为源端口;
connect会激发tcp的三次握手,具体出错情况如下:
-
ETIMEOUT
超时错误tcp发出SYN分节但未响应,超时继续发送直至达到一定的超时时间就会出现此错误;
-
ECONNREFUSED
连接拒绝错误tcp收到的SYN响应为
RST
复位数据包,则表明服务端指定连接的端口没有进程在等待与之连接;产生`RST`数据包的三个条件: 1. 目的地端口的SYN数据包达到后,该端口上没有正在监听的服务器(如前所述); 2. tcp取消已有的一个连接; 3. tcp收到一个根本不存在的连接上的数据包;
-
EHOSTUNREACH 或 ENETUNREACH
未达到错误
若connect失败,则tcp由SYN_SENT转为CLOSED关闭状态,此时sokcet已不再可用,必须关闭重新调用socket创建;
bind()
#include
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
不管对于客户端还是服务端,bind函数都可以不需要;对于客户端connect 如上面所述;对于服务端,内核会根据客户端发送的SYN数据包(段)的目的地址作为服务端的源IP地址,而对于端口,若不指定,则客户端无法获取指定端口,但也可以通过
getsockname
来获取相应的端口;对于客户端其实也可以使用bind函数绑定地址及端口,但一般这么使用;
bind绑定的地址必须为所在主机的网络接口之一
调用bind可以指定ip地址或端口,也可以不指定,具体如下:
bind函数返回错误:
- EADDRINUSE("address already in usr",地址已在使用);
listen()
非阻塞接口,主要指定内核两个队列的最大数;
#include
//baklog为内核为相应的套接字排队的最大连接个数,该值未有一个明确的界限(不能超过资源限制)
int listen(int sockfd, int backlog); //若成功返回0;出错返回-1
根据tcp状态转移图,listen后tcp由CLOSED
变为LISTEN
状态;
为了理解backlog
参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
-
未完成连接队列,每个SYN数据段对应其中的一项
客户端发出SYN数据段并到达服务端,这些套接字处于
SYN_RCVD
状态,服务端正在忙于完成相应的三次握手; -
已完成连接队列,每个已完成的tcp连接三次握手都对应其中的一项,这些套接字处于
ESTABLISHED
;该队列会用于
accept
系统调用,如果该队列为空,则进程会睡眠;若不为空,则会返回队列的队首项给进程;
若两个队列已满,则tcp会忽略SYN数据段,即不会发送RST数据段;因为客户端未收到SYN响应会重发,直至队列不满,且若发送RST,会导致客户端connect错误,进而可能导致客户端退出
accept()
用于从已完成连接队列队头返回下一个已完成连接,如果队列为空,则睡眠等待(若套接字为阻塞方式)
#include
//参数:sockfd为监听套接字,cliaddr为返回的客户端地址,addrlen为客户端协议地址大小(若不需要知道客户端地址及大小,则可置为NULL)
//返回值:新的已连接套接字
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
输入的为监听套接字
,返回的为已连接套接字
,区分两个套接字,是保持监听套接字生命周期一直存在(避免关闭无法获取新的连接),而已连接套接字完成服务即可关闭;
close()
#include
int close(int sockfd);
关闭套接字描述符,但具体应是标记该套接字为已关闭,然后立即返回到调用进程,该套接字不能再被调用进程使用,即不能再read() write()操作;然后tcp将尝试发送已排队等候发送到对端的任何数据,发送完毕后正常的tcp连接终止序列操作;若对端已关闭,是不是应该丢弃缓存区的数据,终止tcp??
shutdown()
#include
int shutdown(int sockfd, int howto); //若成功返回0,失败返回-1
close
关闭套接字存在引用计数
问题,需要引用计数为0时才会标记套接字为关闭,内核尝试发送存在的已排队等候的任何数据,且close
会关闭tcp套接字两个方向的数据传送;
若需要关闭一方的连接,且不需要顾虑引用计数问题,可使用shutdown
来避免;
具体的howto
的值如下:
-
SHUT_RD:关闭连接的读这一半
套接字中不再有数据可接收,并且套接字接收缓存区的现有数据都被丢弃,进程不能对套接字调用任何读函数;对于
tcp套接字
调用shutdown
后,由该套接字接收的对端任何数据都被确认,然后悄然丢弃; -
SHUT_WR:关闭连接的写这一半
对于tcp套接字称为
半关闭
,当前留在发送缓存区的数据将被发送掉,后跟tcp的正常连接终止序列;进程不能再对该套接字调用任何写函数;且不受引用计数影响; SHUT_RDWR:读写连接都关闭,相当于调用两次
shutdown
:第一次调用SHUT_RD
,第二次调用SHUT_WR
;
getsockname() getpeername()
#include
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
getsockname
用于获取绑定的sockfd套接字描述符的本地地址,主要场景如下:
- 未
bind
绑定本地地址及端口或者绑定通配地址或0端口,获取由内核分配的本地地址及本地端口; - 使用
bind
绑定本地地址及为0端口(由内核去选择本地端口)后,获取由内核赋予的本地端口; - 使用
bind
绑定通配地址及本地端口,获取由内核分配的本地地址; - 获取某个套接字的地址族;
getpeername
用于获取绑定的sockfd套接字描述符的外地地址,主要场景如下:
- 服务端
fork
子进程去处理tcp连接后,又执行exec
新的程序,导致新的程序无法获取accept
返回的外地地址,但共享连接套接字
的特性,连接套接字
不会丢失,通过该套接字获取外地地址;
值-结果
对于socket编程接口,如accept
recvfrom
getsockname
getpeername
等函数来获取来源地址或者绑定套接字的地址,其中传入len
长度类似为socklent_t
,且传入的是指针类型,接口调用时该值用于告知内核addr
长度,返回结果是,内核修改addr
返回具体的地址,因此,调用这些接口是需要传入struct sockaddr
长度;
套接字编程异常处理
信号
1. SIGCHLD
对于服务端fork
子进程并发处理请求,若listen
监听主进程不处理已结束的子进程,将会导致子进程称为僵死进程,若存在大量这样进程将占用系统资源导致系统异常;
处理:子进程退出时会向主进程发送
SIGCHLD
信号,父进程应捕获wait
处理;若同时存在大量僵死进程,wait
只会处理第一个停止的子进程,需要循环使用waitpid
来处理子进程状态并回收系统资源;
2. SIGPIPE
对于服务端进程崩溃或异常终止情况,服务端进程退出会关闭套接字描述符,进而会发送FIN
数据段至客户主机,服务端tcp状态处于FIN_WAIT_2
,客户端tcp状态处于CLOSE_WAIT
;若此时客户端继续write
发送数据,则对于第一次发送,会收到服务端主机的RST
数据段;若再次write
系统调用发送数据,则内核会向客户进程发送SIGPIPE
信号;
对于
SIGPIPE
信号,客户端应忽略处理,否则默认处理该信号为终止进程;
3. 被中断的系统调用
慢系统调用,如阻塞的网络系统调用:read
write
accept
等,阻塞期间被信号中断,如SIGCHLD
信号,就会返回EINTR
错误;
需要对中断处理错误继续重复系统调用;
accept()调用错误
客户端与服务端三次握手建立连接后,客户主机向服务端发送
RST
数据段,这时服务端再调用accpet
就会返回ECONNECABORTED
错误(具体看linux内核实现);
该错误为非致命错误,因此重复调动
accpet
即可;
(已连接)服务主机崩溃、崩溃后重启、关机
服务主机崩溃或者网络不可达或者崩溃后重启,都会导致:
如果a进程阻塞在
read
上,那么结果只能是永远的等待。如果a进程先
write
然后阻塞在read
,由于收不到B机器TCP/IP栈的ack,TCP会持续重传12次(时间跨度大约为9分钟),然后在阻塞的read调用上返回错误:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH
假如B机器恰好在某个时候恢复和A机器的通路,并收到a某个重传的pack,因为不能识别所以会返回一个RST,此时a进程上阻塞的read调用会返回错误
ECONNREST
对于关机,服务端进程结束后会发送FIN
数据段;
对于以上情况,客户进程无法有效及时感知到服务端异常情况
处理:tcp keepalive或者应用层心跳
数据格式
对于应用数据存在系统内存存储大小端字节序问题,如果双方字节序不同,会造成数据处理异常;
以相同字符集及字节序处理;
I/O复用
对于unix系统I/O模式如下:
阻塞I/O
如read recvfrom等非阻塞I/O
指定recvfrom模式为非阻塞,调用返回EWOULDBLOCK
错误;I/O复用(如select poll)
-
信号驱动I/O(SIGIO)
-
异步I/O(POSIX的aio_系列函数)
select()
#include
#include
struct timeval {
long tv_sec;//秒
long tv_usec;//微妙
}
//若有就绪描述符就返回其数目,若超时返回0,若失败返回-1
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *excepset,
const struct timeval *timeout);
select若有就绪描述符就会set相应的位,若再次select操作,需要重置fd_set相应的位;
-
timeout参数
类型为const,因此不会被修改,若需要测量select时间需要获取前后时间;
- 若timeout为0,不阻塞;
- 若为NULL,则一直阻塞;
- 若不为0,则为超时时间;
-
maxfd
为最大描述符值+1,注意不是最大描述符数目,此值用于内核检索描述符的范围区间;
-
fd_set读、写、异常描述符集
使用FD_SET FD_CLR FD_ISSET FD_ZERO 这些宏函数来设置或测试描述符集;
就绪条件
其中有数据
可读或者可写
,指接收缓存区或者发送缓存区数据字节数大于等于套接字缓存区低水位标记的大小,该标记可通过SO_RCVLOWAT
或SO_SNDLOWAT
选项修改;
连接关闭
,指若为读关闭(接收了FIN的tcp连接),则读不会阻塞且返回0(即返回EOF
);若为写关闭,则再写操作就会产生SIGPIPE
信号;
注意若不是套接字描述符,如标准输入输出,则可读即内核缓存区存在数据可读,select不会感知用户缓存区的大小,因此,混合使用stdio与select需要小心,可参考select函数与stdio混用的不良后果
适用场景
多描述符情况下,可通过select判定哪些描述符可读写或异常,并且使用select不会存在无法感知对端异常崩溃或者连接关闭等情况(如使用其他阻塞调用未读写套接字,就会导致阻塞调用阻止获取对端异常情况);
-
使用
select
替换掉fork
子进程来单进程accept
多连接;select
监听套接字是否可读,可判定是否有新的连接请求,再调用accept
获取已连接套接字,并select
监听所有套接字来读写数据;
pselect()
#include
#include
#include
struct timespec {
long tv_sec; //秒
long tv_nsec; //纳秒
}
int pselect(int maxfd, fd_set *readset, fd_set *writeset, fd_set *excepset,
const struct timespec *timeout, const sigset_t *sigmask);
与select
区别,是超时时间类型发生变化,支持纳秒;其次,添加了信号屏蔽字,pselect
期间可屏蔽指定的信号,完成调用自动恢复;
poll()
#include
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,
通过遍历文件描述符来获取已经就绪的socket
。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
套接字选项
SO_KEEPALIVE
tcp套接字保活选项,若在指定时间内(由系统配置决定)该套接字的任一方向都没有数据交换,tcp就自动给对高端发送一个保持存活探测分节(keep-alive probe)
,这是对端必须相应的tcp数据段,会导致以下三种情况:
对端响应ACK数据段,应用进程不会得到通知(因为一切正常),再等待指定时间后,tcp将会再次发出一个保活探测分节;
对端响应
RST
,以通知本端tcp:对端已崩溃且已重新启动。该套接字的待处理错误会被置为ECONNRESET
,套接字本身会被关闭;-
对端无响应,tcp会尝试再次发出保活探测分节;
若根本无响应,则套接字的待处理错误置为
ETIMEOUT
,套接字本身被关闭;若收到
ICMP错误
,则会返回相应的错误,套接字本身被关闭;如常见的ICMP错误是"host unreachable"(主机不可达),说明对端主机可能并没有崩溃,只是不可达(如中间路由异常),待处理错误置为EHOSTUNREACH
;
SO_RCVBUF SO_SNDBUF
每个套接字都会有一个接收缓存区和发送缓存区,对于tcp缓存区大小限定tcp滑动窗口的大小,若发送数据长度查过该大小,tcp就会丢弃该数据段;对于udp,不存在流量控制,若接收到的数据报超过缓存区大小,就会被丢弃
设置缓存区大小值时,注意函数调用顺序!
对于tcp,窗口大小选项是在建立连接三次握手时交换得到,因此,客户端端来说,需要在connect
前设置;服务端来说,需要在listen
前设置;
SO_REUSEADDR SO_REUSEPORT
SO_REUSEADDR
套接字选项具有以下不同作用:
-
SO_REUSEADDR
允许启动一个监听服务器并捆绑众所周知的端口,即使以前建立的将该端口用作本地端口的连接仍存在; - 允许在同一端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的IP地址即可;但不允许绑定同一个地址和端口在不同服务器上;
- 允许单个进程绑定同一端口到多个套接字上,只要绑定不同的本地ip地址即可;
- 允许完全重复的绑定:如果一个ip地址和端口已绑定到某个套接字上,还可以绑定到另一个套接字上,但前提是传输协议支持,一般仅支持UDP;
getsockopt setsockopt
#include
//若成功返回0,否则-1
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
fcntl函数
与文件控制的名字相符,该函数执行各种描述符控制操作,如修改非阻塞I/O;
UDP 数据报传输协议
udp为无连接、无状态、不可靠的传输层协议,其中sokcet
建立需要指定类型为SOCK_DGRAM
;
常用的无连接udp传输函数流程如下:
具体的函数使用如下:
#include
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr *fromaddr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
const struct sockaddr *toaddr, socklen_t addrlen);
对于后两个参数,recvfrom
同accept
类似,都是接收对端的地址;sendto
与connect
类似,都是发送或者建立连接的地址;每个tcp套接字都有接收/发送缓存区,但udp套接字只有接收缓存区,没有发送缓存区,从概念上说udp有数据就发送(不管对方是否接收)不会去缓存,不需要发送缓存区,且udp也可以发送空数据;当udp接收缓存区满时,继续接收的数据会被丢弃;可通过SO_RCVBUF
选项来修改其大小;
tcp也可以使用这两个函数,不过不常见;
对于无绑定的的udp套接字来说,sendto
会触发内核自动绑定本地地址及临时端口,也可以通过bind
绑定地址到套接字,则sendto
就不需要指定发送地址(若指定就失败);
已连接udp套接字
可使用connect
函数来指定对端的地址,后续read
send
write
等数据接收/发送函数就不需要指定地址;
connect函数udp不同于tcp会建立三次握手,它不会发送任何消息,只是保存对端的地址,调用后立即返回,同时若在未绑定的套接字上,内核会绑定临时端口;
使用connect
已连接的udp与未连接的udp区别如下:
已连接的udp不需要每次发送数据都指定对端地址;
指定对端地址后,对于不是该地址的数据报都不被应用层接收;
-
对于无连接的udp,数据发送后应用层无法接收到对端的异常错误(内核会接收到该消息),而已连接的udp会应用层返回异步错误;
如
port unreachable
内核会该icmp错误转化为ECONNREFUSED
错误,对于由err_sys
函数输出错误为connection resused
;
其他作用: 再次调动
connect
可修改udp套接字上的地址,对于tcp不能两次调用connect
;断开已连接的udp:将地址族成员
sin_family
修改为AF_UNSPEC
,则会返回EAFUNSUPPORT
错误,不过没关系;
使用已连接的udp套接字,可减少每次连接套接字的步骤,因此,相比无连接的udp套接字,性能提升;
对于connect
调用只是告知内核对端的地址,若是服务端未调用connect
,且服务端多连接地址,仍需要通过recvfrom
来获取客户端的地址
其他
udp无流量控制,对于慢速服务端系统,可能导致应用层数据报丢包或者中间路由器丢失等;
可结合select
函数判定udp是否可读来接收发送数据;
使用建议
- 对于单播和广播必须使用udp
- 对于简单地请求应答可以使用udp,不过需要添加错误检测功能(如确认、超时及重传机制);
- 对于海量数据(如文件发送)不建议使用udp(除了添加上一条的特性外,还需要添加窗口控制、拥塞避免和慢启动等特性,基本再造tcp);
SCTP 流量控制传输协议
SCTP(Stream Control Transmission Protocol,流量控制传输协议)是IETF(Internet Engineering Task Force,因特网工程任务组)在2000年定义的一个传输层(Transport Layer)协议,是提供基于不可靠传输业务的协议之上的可靠的数据报传输协议。
该协议被Linux2.6内核版本
吸纳,但目前暂不支持macOS Windows系统;
名字与地址转换
对于
gethostbyname
gethostbyaddr
getservbyname
getservbyport
getaddrinfo
getnameinfo
等函数,是通过查询本地/etc/hosts
或者使用/etc/resolv.conf
配置文件获取dns服务器地址,进而通过udp查询相应的域名信息;
具体函数用途:
gethostbyname
通过主机名查询ipv4
地址;gethostbyaddr
相反,通过ipv4
地址查询主机名;
getservbyaddr
通过服务名查询端口;getservbyport
通过端口查询服务名;
getaddrinfo
与getnameinfo
为协议无关转换函数,分别用于主机名字和ip地址之间和服务名字和端口号之间转换;
gethostbyname
gethostbyaddr
为不可重入函数,因为使用静态变量存储获取信息,不过可使用可重入版本gethostbyname_r
gethostbyaddr_r
版本;
gethostbyname()
#include
struct hostent {
char *h_name; //查询主机的规范名字,如dns查询的CNAME记录名称
char **h_aliases; //主机别名指针数组,以NULL为结束符
int h_addrtype; //主机地址类型(AF_INET)
int h_addrlen; //主机地址长度(4)
char **h_addr_list;//获取ipv4地址指针数组,以NULL为结束符
}
struct hostent *gethostbyname(const char *hostname);
不同于其他套接字函数,该函数若失败,不会设置
errno
值,而是设置h_errno
全局变量,并可通过hstrerror
函数返回具体的错误描述;*具体的错误如下:
- HOST_HOT_FOUND
- TRY_AGAIN
- NO_RECOVERY
- NO_DATA(等同NO_ADDRESS),表示无记录
gethostbyaddr()
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family)
与gethostbyname
相反,通过ipv4地址返回主机名信息;
getservbyname() getservbyport()
#include
struct servent {
char *s_name; //服务名
char **s_alias; //别名列表
int s_port; //端口名,网络字节序
char *s_proto; //使用的协议
}
//必须指定servname
//protoname为协议名称,如"tcp" "udp"
struct servent *getservbyname(const char *servname, const char *protoname);
struct servent *getservbyport(int port, const char *protoname);
返回的servent结构体中端口是以网络字节序返回的,可直接用于套接字端口地址;
getaddrinfo()
#include
struct addrinfo {
int ai_flag; //AI_XX
int ai_family; //AF_XX
int ai_socktype; //SOCK_XX
int ai_protocol; //0或者IPPROTO_XX for ipv4 or ipv6
socklen_t ai_length; //ai_addr长度
char *ai_cannoname;
struct sockaddr *ai_addr; //获取地址信息
struct addrinfo *ai_next; //结构体链表地址
}
//hints字段为需要获取的指定信息
int getaddrinfo(const char *hostname, const char *service,
const struct addrinfo *hints, struct addrinfo **result);
获取的struct addrinfo **result
为动态内存分配,使用完成需要使用freeaddrinfo
函数释放,若失败,可通过gai_strerror
函数获取失败信息;
系统守护进程
syslogd
系统日志守护进程,macOS具体介绍见man syslogd
The syslogd server receives and processes log messages. Several modules receive input messages through various channels, including UNIX domain sockets associated with the syslog(3), asl(3), and kernel printf APIs, and optionally on a UDP socket from network clients.
The Apple System Log facility comprises the asl(3) API, a new syslogd server, the syslog(1) command-line utility, and a data store file manager, aslmanager(8). The system supports structured and extensible messages, permitting advanced message browsing and management through search APIs and other components of the Apple system log facility.
大概意思是:syslogd
守护进程主要用于接收和处理日志消息,包含unixt
域套接字关联的syslog
asl(apple system log)
内核日志, 以及udp
套接字的网络客户端;
主要流程是:
- 系统启动后由launchd fork出syslogd,进程启动后读取
/etc/syslog.conf /etc/asl.conf
等配置文件; - 创建`unix域套接字以监听系统日志相关的服务请求;
- 创建
udp
套接字,绑定固定的端口(bsd系统应该是514,mac不详); - 打开路径名
/dev/klog
,内核的任何消息都是这个设备的输入;
注意:
- syslog接口配置
asl
日志级别,需要修改/etc/asl.conf
其中默认级别是notice
-
NSLog
接口被设计为error log,是ASL的高层封装
NSLog会向ASL写log,同时向Terminal写log,而且同时会出现在
Console.app
中(Mac自带软件,用NSLog打出的log在其中全部可见);不仅如此,每一次NSLog都会新建一个ASL client并向ASL守护进程发起连接,log之后再关闭连接。所以说,当这个过程出现N次时,消耗大量资源导致程序变慢也就不奇怪了;建议使用
CocoaLumberjack
开源库;
关于mac下的syslog系统
NSLog效率低下的原因
inetd
该进程功能在Mac系统已被整合进launchd
进程;
daemon()
#include
//Unless the argument nochdir is non-zero, daemon() changes the current working directory to the root (/).
//Unless the argument noclose is non-zero, daemon() will redirect standard input, standard output, and standard error to /dev/null.
int daemon(int nochdir, int noclose);
系统提供了进程称为守护进程的接口(不过在Mac上已被废弃,但仍可使用),具体流程与《Unix环境高级编程》
中称为守护进程流程一致,不过注意最后一步会使用openLog()
函数启动syslog
日志接口,即创建unix
套接字来连接syslogd
;
高级I/O函数
套接字I/O操作上设置超时时间的方法:
-
调用
alarm
, 指定超时时间满后产生SIGALARM
信号该方法对于多线程正常使用信号非常困难
select
超时作为定时器超时阻塞;-
使用套接字选项
SO_RCVTIMEO
SO_SNDTMEO
为recvfrom
sendto
设置超时一旦设置选项,整个套接字都会生效,优势是:一次性设置选项;
不可移植的超时方法:
/dev/poll
文件-
FreeBSD引入的
kqueue
接口(Mac继承FreeBSD是支持的)本接口允许进程向内核注册描述所关注
kqueue
事件的事件过滤器(event filter),除了与select
所关注的类似文件I/O和超时外,还有异步I/O、文件修改通知、进程跟踪和信号处理;见OSX/iOS中多路I/O复用总结
接收/发送函数
recv() send()
#include
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, void *buff, size_t nbytes, int flags);
与read
write
类似,且前三个参数相同,不同是最后一个参数flags
标志位,可设置为0;
readv() writev()
#include
struct iovec {
void *iov_base; //缓存区的起始地址
size_t iov_len; //缓存区的长度
}
//iov为iovec结构体数组的指针,iovcnt为结构体的个数
ssize_t readv(int fields, const struct iovec *iov, int iovcnt);
ssize_t writev(int fields, const struct iovec *iov, int iovcnt);
与read
write
函数类似,但其允许单个系统调用读入或写出一个或多个缓存区,分别称为分散读
和集中写
,用于*读取应用不连续内存数据或将不连续内存数据写出,而不需要多次调用read
write
;
recvmsg() sendmsg()
#include
struct msghdr {
void *msg_name; //协议地址,用于指定或者接收
socklen_t msg_namelen; //协议地址长度
struct iovec *msg_iov; //iovec结构体数组指针
int msg_iovlen; //iovec结构体数组长度
void *msg_control; //辅助数据地址
socklen_t msg_controllen;//辅助数据大小
int msg_flags; //标志位
}
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);
可取代read
readv
recv
和recvfrom
的通用I/O函数;
unix域套接字
unix域套接字
主要用于单主机本地通信,为常用的IPC通信之一,其具有如下优势:
- 相比tcp/udp通信,
unix域套接字
不需要经过网络协议栈,不需要经过封包解包操作,速度快;且不需要指定ip地址,而是通过本地文件; - 可跨进程传递描述符(不是简单地传递描述符号,而是共享描进程打开的文件表项,创建新的描述符);
- 较新的实现把客户端的凭证(用户ID和组ID)提供给服务端,用于额外的安全检查;
unix域套接字
的地址结构如下:
struct sockaddr_un {
sa_family_t sun_family; //AF_LOCAL(POSIX命令) or AF_UNIX,两个值相同
char sun_path[104]; //路径地址名称,需要绝对地址,若为相对地址,客户/服务进程需要在同一路径下
}
unix域套接字
可重用tcp/ip通信的接口函数,且支持字节流类型(类似tcp,提供无边界字节流接口)及数据报类型(类似udp,提供保留边界记录的不可靠的数据报服务),但具有如下不同:
-
unix套接字
需要绑定路径,而不是ip地址
该路径不能为空,且对于服务端
bind
前需要unlink
删除该路径,否则会导致绑定失败;对于客户端,该路径需要明确是socket
类型,且具有访问权限;bind
绑定时会自动生成socket
类型文件,该文件权限默认为0777
,需要考虑到进程的umask
文件屏蔽字,可能存在差异;对于字节流类型,
connect
前可不需要绑定(也可以绑定);但对于数据报类型,sendto
发送函数内核无法自动绑定地址,需要使用前绑定与服务端相同地址; -
对于字节流数据服务,若
connect
连接时发现对端监听套接字队列已满,则返回ECONNRESUSED
错误;而对于
tcp
则不会ACK
响应,客户端将数次发送SYN
分段重试,直至超时或者路由不可达等错误; -
对于未绑定的
unix域套接字
上发送数据报不会自动给这个套接字绑定路径名,这不同于tcp/udp套接字
:需要sendto
或者connect
连接对端时,会自动绑定本地的临时端口;对于字节流
unix域套接字
,connect
建立连接时,内核会自动绑定客户端的临时路径,不需要bind
不同的路径,但绑定也可以;注意事项
bind
绑定地址长度,可使用sizeof(struct sockaddr_un)
结构体的长度,或者使用包含sun_path
字符串长度,bind
函数会自动获取su_path
路径名,但必须保证输入长度不要超过结构体的长度,防止栈溢出
;-
bind
绑定地址sun_path
路径名可为空字符串作为抽象路径名
,但Mac上不可行;若为空字符串,则等同于
ipv4 INADDR_ANY
或者ipv6 INADDR_ANY_INIT
常值,不过依然存在路径名,只是不会创建该文件,具体可参考抽象 unix 域套接字地址 -
若unix域套接字路径文件被删除是否有影响?
若服务端及客户端建立连接后,因为文件存在引用计数概念,只要有进程持有该文件,内核直至该文件引用计数为0时才会释放删除它,因此无影响!
若对于服务端绑定监听后而客户端未连接时删除,则客户端无法连接到服务端;
若服务端停止后文件域套接字文件未删除,客户端
connect
连接,则无法连接,因为连接成功需要当前有一个打开的绑定了域套接字文件的域套接字;
setsockpair()
socketpair函数可以创建两个连接起来的unix域套接字:
#include
int socketpair(int family, int type, int protocol, int sockfd[2]);
socketpair
的参数中family必须为AF_LOCAL,protocol必须为0,type可以为SOCK_STREAM
或SOCK_DGRAM
,新创建的两个套接字描述符将作为sockfd[0]
和sockfd[1]
返回。类似管道形式,但为流管道
。
传递描述符
当我们需要传递描述符时,通常可以使用方法有:
- fork调用返回以后,子进程共享父进程的所有描述符
- exec调用执行后,所有的描述符通常保持打开状态
第一种方式里,我们可以把描述符从父进程传递到子进程,然而我们也可能需要在子进程传递描述符到父进程。unix系统提供了用于从一个进程向其他任意进程传递描述符的方式,而这两个进程不需要有任何亲缘关系。这种技术要求在两个进程之间创建一个uds,然后使用sendmsg通过这个uds发送特殊结构的消息。这个特殊的消息会由内核处理,把打开的描述符从发送进程传递到接收进程。
通过uds传递描述符的步骤具体如下:
- 创建一个字节流或数据报的uds。这可以通过调用socketpair然后父子进程之间的连接;也可以使用套接字API。通常建议使用字节流套接字而不是数据报套接字,因为使用数据报套接字并没有什么好处,反而还存在数据报被丢弃的可能。
- 发送端打开描述符。uds可以传递各种类型的描述符,而不是仅包括文件描述符。
- 发送端进程创建一个msghdr的结构,其中含有待传递的描述符,然后调用sendmsg将其发送出去。发送一个描述符会使其引用计数加一。posix规定需要作为辅助数据发送描述符;
- 接收端进程调用recvmsg在创建的uds上接收描述符。这个过程会在接收进程创建一个新的描述符,然后将其指向和发送进程发送的描述符指向的同一个内核文件选项。所以接收端收到的描述符不同于发送端发送端描述符时很正常的。
msghdr的结构定义:
/*
* [XSI] Message header for recvmsg and sendmsg calls.
* Used value-result for recvmsg, value only for sendmsg.
*/
struct msghdr {
void *msg_name; /* [XSI] optional address */
socklen_t msg_namelen; /* [XSI] size of address */
struct iovec *msg_iov; /* [XSI] scatter/gather array */
int msg_iovlen; /* [XSI] # elements in msg_iov */
void *msg_control; /* [XSI] ancillary data, see below */
socklen_t msg_controllen; /* [XSI] ancillary data buffer len */
int msg_flags; /* [XSI] flags on received message */
};
具体的例子就暂时不列举了。
验证发送者的身份
可以用uds传递的另一种辅助数据就是用户凭证。用户凭证的数据结构在不同的操作系统中并不一致,这里就不再详细介绍了。不过需要使用
非阻塞I/O
套接字默认是阻塞式,若使用非阻塞式,需要通过fcntl
函数设置套接字为O_NONBLOCK
,具体如下:
//获取当前套接字标志
int flags = fcntl(sockfd, F_GETLF, 0);
if (flags < 0) {
printf("fcntl err:%s", strerror(errno));
}
//设置当前套接字标志
fcntl(sockfd, F_SETFL, flags|O_NONBLOCK);
具体分类如下:
-
输入操作,如
read
readv
recvfrom
recv
recvmsg
五个函数对于阻塞式操作,
tcp
类型套接字需要等待内核接收缓存区有数据可读(单个字节或多个字节,可通过指定MSG_WAITALL
标志位[需要支持]来指定读取固定数目字节),且从内核缓存区拷贝至用户缓存区才会返回,否则会当前进程会睡眠,直至有数据可读;udp
类型,需要等待有数据报可读;对于非阻塞式,若无数据可读都会返回错误
EWOULDBLOCK
; -
输出操作,如
write
writev
sendto
send
sendmsg
五个函数对于阻塞式,
tcp
类型需要内核发送缓存区有空间写,且将用户缓存区拷贝至内核缓存区才会返回(返回的为写入缓存区的字节数),否则进程会睡眠;对于非阻塞式,
tcp
类型会返回EWOULDBLOCK
错误;udp
类型不存在发送缓存区概念,会直接发送数据报; -
接受外来连接操作,如
accept
函数对于阻塞式,会一直等待已完成连接队列存在连接,否则进程睡眠;
对于非阻塞式,若尚无新的连接会返回
EWOULDBLOCK
; -
发出连接操作,如
connect
函数对于阻塞式,
tcp
类型connect
函数会直至三次握手完成才会返回;udp
类型实质是内核保存对端的地址和端口,立即返回(不会阻塞);对于非阻塞,若
tcp
类型connect
连接未完成会返回EINPROGRESS
错误,不同于上面的套接字函数,通常单主机情况(客户和服务端在同一主机)会立即连接返回;
ioctl操作
#include
int ioctl(int fd, int request, ...);//成功返回0,否则-1
网络程序经常使用ioctl
获取所在主机全部网络接口的信息,包括:接口地址、是否支持广播、是否支持多播,等待;
kcp快速可靠协议
KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。
整个协议只有 ikcp.h, ikcp.c两个源文件,可以方便的集成到用户自己的协议栈中。也许你实现了一个P2P,或者某个基于 UDP的协议,而缺乏一套完善的ARQ可靠协议实现,那么简单的拷贝这两个文件到现有项目中,稍微编写两行代码,即可使用。
与tcp
相比,同样拥有发送确认、窗口控制、慢启动及拥塞控制,但其可关闭慢启动及拥塞控制,并且改变了RTO超时时间策略,可选择性重传丢失包,可调节延时发送ACK
;TCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。可靠UDP,KCP协议快在哪?
具体应用层使用:
kcp
纯算法实现,需要外部传入当前时间戳及底层udp
发送接口即回调函数(用于kcp_flush
调用发送数据报),外部需要间隔10ms或者100ms来循环调用kcp_update
来更新kcp状态,kcp_update
会触发调用kcp_flush
来负责数据的超时重传、快速重传、选择性重传、数据正常发送、数据报响应等;
kcp_send
负责将应用层用户数据进行分片(若超过mtu),并将其snd_queue
发送队列中;
kcp_input
负责将recv_buff
接收缓存中数据解包并重新组装更新接收队列recv_queuq
可靠地供应用层获取数据,其中包含了数据类型、长度校验,对数据报类型IKCP_CMD_PUSH
IKCP_CMD_ACK
IKCP_CMD_WASK
IKCP_CMD_WINS
类型数据报处理,并进行流量控制及拥塞控制;
kcp_recv
负责将recv_queue
接收队列中的数据进行合并,并拷贝至用户缓存区;
源码解析参考:
KCP: 快速可靠的ARQ协议
KCP原理及源码解析
网络传输协议kcp原理解析
带外数据
许多传输层有
带外数据(out-of-band data)
概念,有时也成为经加速数据(expedited data)
。主要用于已连接某端发生了重要事情,能迅速通告对端,“迅速”是指通知应该在已经排队等待发送的任何“普通”数据前发送;
tcp
并没有真正的带外数据
,但其提供了紧急模式
和 紧急指针
,udp
未实现带外数据
概念;
使用
发送带外数据:
send(sockfd, ‘a’, 1, MSG_OOB);//其中MSG_OOB指带外标记
发送进程通过send
调用指定带外标志MSG_OOB
来发送带外数据,不过该调用只有最后一个字节为带外数据
,即带外数据只有一个字节;接收进程tcp
收到新的紧急指针后,可通过SIGURG
信号,或者通过select
异常处理 来接收通知;
带外数据未广泛使用,一般用于心跳机制;
原始套接字
原始套接字提供了tcp
udp
无法提供的能力:
- 可以读或写
icmpv4
icmpv6
igmpv4
等分组,直接在用户进程处理; - 可以读写内核不处理其协议字段的
ipv4
数据报(大多数内核只处理icmp
igmp
tcp
udp
数据报); - 进程可以使用
IP_HDRINCL
套接字选项自行构造ipv4
首部;
具体使用:
int sockfd = socket(AF_INET, SOCK_RAW, protocol);//其中protocol参数是IPPROTO_XXXX的某个常值,如IPPROTO_ICMP
只要超级用户才能创建原始套接字,防止普通用户往网络写自行构造的IP数据报,但Mac系统自带的ping程序不需要提权操作,具体原因是:
socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
其使用了SOCK_DGRAM
类型及IPPROTO_ICMP
选项,
这种创建这种套接字是合法的,但并非所有的平台都能创建,这还是要取决于内核/proc/sys/net/ipv4/ping_group_range 这个属性值,是一对整数,指定了允许使用 ICMP 套接字的组 ID的范围(可修改,需要权限)。在Linux一些版本比如Ubuntu,centos,这个默认值是0 1,意味着没人能够使用这个特性,在Android上这个范围是0 2147483647,意味着进程都可以创建这种套接字。Mac也是可以的,所以也说明了为什么ubuntu下的ping是带s位的,而Mac和Android设备上的ping是不用带的,因为使用这种socket已经可以达到ping的功能。
————————————————
原文链接:https://blog.csdn.net/aa642531/java/article/details/85461294
其中原始套接字还可以使用bind
绑定本地地址(仅能绑定本地地址,因为原始套接字没有端口的概念),如果不绑定,内核就会自动绑定外出接口的地址;
原始套接字还可以使用connect
函数来确定目的地址(仅目的地址,原始套接字没有端口概念),后续就可以使用send
write
等函数取代sendto
;
设定IP_HDRINCL
选项:
const int on = 1;
setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));
原始套接字输出
原始套接字还提供了一个非常有用的参数IP_HDRINCL:
1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。
2、如果不开启该参数:我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。
原始套接字输入
调试技术
如
tcpdump
wireshark抓包工具
抓取完整的数据,dtrace
系统调用跟踪工具,netstat
查看哪些ip地址及端口在使用、tcp状态及路由信息等, lsof
查看端口占用及所属进程、进程id、用户、描述符fd、类型、文件的大小或偏移、协议类型及名称等;
其它的如
ping
icmp协议查看对方是否可达,route
添加或删除路由表,networksetup
修改静态路由,等;
源码下载编译
下载地址:www.unpbook.com,也可以使用其他人构建的git分支,如https://github.com/DingHe/unpv13e.git(
编译运行
Execute the following from the src/ directory:
./configure # try to figure out all implementation differences
cd lib # build the basic library that all programs need
make # use "gmake" everywhere on BSD/OS systems
cd ../libfree # continue building the basic library
make
cd ../libroute # only if your system supports 4.4BSD style routing sockets
make # only if your system supports 4.4BSD style routing sockets
cd ../libxti # only if your system supports XTI
make # only if your system supports XTI
cd ../intro # build and test a basic client program
make daytimetcpcli
./daytimetcpcli 127.0.0.1
注意:网上教程需要将
libunp.a
unp.h
静态库及头文件放置/usr/local/lib
/usr/local/include
,其实makefile已经链接了当前目录文件;
遇到的问题
- 函数定义冲突
屏蔽inet_ntop.c中的inet.h
链接系统头文件,再次make
编译; - libxti not found
编译说明只需要支持XTI系统才需要,暂不编译; - Ld:symbol(s) not found for architecture x86_64
查看makefile
文件系统已经链接静态库libunp.a
,且nm libunp.a
查看静态库符号存在该符号;
➜ intro git:(master) ✗ file ../libunp.a
../libunp.a: current ar archive
➜ intro git:(master) ✗ lipo -info ../libunp.a
Non-fat file: ../libunp.a is architecture: x86_64
通过file
查看静态库文件类型为ar archive
(压缩格式),通过lipo -info
查看支持x86_64
;
查看
libunp.a
内部包含的*.o文件,其中包含了error.o
,但仍然存在符号找不到情况。
直接链接error.o
文件,编译通过;
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o daytimetcpcli daytimetcpcli.o ../lib/error.o
通过添加libunp.a
静态库并改变与error.o
的链接顺序,都编译通过!,因此排除gcc链接问题,应该是libunp.a
静态库问题,导致ld
无法链接该静态库;
注意:ld:warning: ignoring file提示,因此ld链接时忽略了静态库!
网上遇到了类似问题:在macOS-Mojave上编译Lua失败的经历
You must use the correct ar (archiver), e.g.:
make CXX=o64-clang++ AR=x86_64-apple-darwin1X-ar
O�therwises it uses the system ar and won’t work.
StackOverFlow 上也有一个相似的问题
说是使用的 ar 命令不对(或者是说 GNU 和 macOS ar命令的行为不太一样)。隐约记得我是装了一个 binutils 的,难道被覆盖了。
于是查了一下:
which ar ranlib
/usr/local/opt/binutils/bin/ar
/usr/local/opt/binutils/bin/ranlib
在~/.zshrc
环境变量配置文件中发现指定了ar的链接路径;
具体问题:猜测是GNU的ar/ranlib 与 macos工具不一致导致
【解决】
修改src/Make.defines中的ranlib路径及lib/Makefile中的ar路径;
- connection refused
原因是获取时间的服务器(port:13)没有运行,intro/
下有个daytimetcpsrv.c文件在另一个终端窗口下make后运行该服务器程序即可,如下;
小知识
1. gcc链接顺序问题
gcc链接存在链接顺序问题,如静态库(.a就是包含了许多.o文件)**
gcc -l 解释如下:
-l library
Search the library named library when linking. (The second alter-
native with the library as a separate argument is only for POSIX
compliance and is not recommended.) It makes a difference where in the command you write this option;
the linker searches and processes libraries and object files in the
order they are specified. Thus, foo.o -lz bar.o searches library z
after file foo.o but before bar.o. If bar.o refers to functions in
z, those functions may not be loaded.这句话翻译过来的意思就是说,如果你的库在链接时安排的顺序是:foo.o -lz bar.o。那么gcc的链接器先搜索库foo,然后是z库,然后是bar库。
这样就带来一个问题,如果库bar调用了库z里面的函数,但是链接器是先搜索的库z,这时候并没有发现库bar调用库z啊,所以就不会把库z中的这部分函数体挑出来进行链接。
2.ar ranlib
ar 命令
这个命令是将多个 obj 文件打包成一个静态 .a 库文件。其用法类似于压缩命令啊。
ranlib
这个命令会将 ar 打包后的文件,里面所有的 object 文件定义的符号,生成一个索引存在里面。可以加快链接速度。 GNU 的 ranlib 命令是和
ar -s
命令等价的。
概念
-
包裹函数
对系统函数错误自定义的错误处理函数,用于处理系统函数错误(如打印错误信息,崩溃处理等);
-
协议数据单元(protocol date unit, PDU)
计算机网络各层对等实体间交换的单位信息称为
协议数据单元
,分节(segment)
就是对应tcp传输层的PDU,应用层交换的PDU称为应用数据
,网络层交换的PDU称为ip数据报
,链路层交换的PDU称为数据帧
-
accept() close()
accpet()内核会执行tcp的三次握手,close()内核会执行tcp的四次挥手;
-
原始套接字(raw socket)
应用层绕过传输层直接使用网络层的套接字;
-
与数据链路层通信应用
tcpdump
或者BSD分组过滤器(BSD packet filter, BPF)
接口(通过该接口在源自berkeley的内核中找到 )或者数据链路层提供者接口(datalink provider interface, DLPI)
通常随svr4内核提供, 直接与数据链路层通信; -
保护消息边界
保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息,也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包. 而面向流则是指无保护消息保护边界的,如果发送端连续发送数据, 接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
-
FIN
无论进程异常退出还是正常退出,内核会将进程所有打开的文件描述符全部关闭,因此所有的tcp连接未关闭的,如处于
close_wait
、FIN_WAIT2
状态的,都会发出FIN
数据包,最终FIN_WAIT2
状态就会转移为TIME_WAIT
状态,确认主动方发出的ACK
确认包已经抵达被动方; -
TIME_WAIT
该状态存在的理由:
可靠地实现tcp全双工连接的终止;
-
允许老的重复分节在网络中消逝;
使用上一次的tcp连接,若tcp处于TIME_WAIT状态,则创建连接会失败;除非新的连接SYN序列号大于前一化身的结束序列号;
-
套接字对(socket pair)
一个tcp连接的
套接字对
是一个定义该链接的两个端点的四元组:本地ip地址、本地端口、远端ip地址、远端端口;标识每个端点的两个值(ip和端口)通常称为一个套接字
;
参考资料
0-MacOS Catalina下Unix网络编程环境搭建详细教程
附录
https://github.com/FengyunSky/notes/blob/master/study/system/unix/kcp.tar
https://github.com/FengyunSky/notes/blob/master/study/system/unix/unpv13e.tar