unix网络编程

套接字地址结构

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套接字地址结构需要强制类型转换

主机字节序

内存存储多字节数据由于不同系统存储方式不同,将某个给定系统所用的字节序叫做主机字节序,分为两种模式:

  • 小端字节序

    低序字节在低地址,高序字节在高地址;

  • 大端字节序

    与小端相反,低序字节在高地址,高序字节在低地址;


    image.png

网络字节序

网络协议必须指定一个网络字节序,网际协议使用大端字节序来传送多字节整数(如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代表shortl代表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的三次握手,具体出错情况如下:

  1. ETIMEOUT超时错误

    tcp发出SYN分节但未响应,超时继续发送直至达到一定的超时时间就会出现此错误;

  2. ECONNREFUSED连接拒绝错误

    tcp收到的SYN响应为RST复位数据包,则表明服务端指定连接的端口没有进程在等待与之连接;

    产生`RST`数据包的三个条件:
    1. 目的地端口的SYN数据包达到后,该端口上没有正在监听的服务器(如前所述);
    2. tcp取消已有的一个连接;
    3. tcp收到一个根本不存在的连接上的数据包;
    
  3. EHOSTUNREACH 或 ENETUNREACH未达到错误

    image.png

    若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地址或端口,也可以不指定,具体如下:


image.png

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系统调用,如果该队列为空,则进程会睡眠;若不为空,则会返回队列的队首项给进程;

    image.png

    若两个队列已满,则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()调用错误

image.png

客户端与服务端三次握手建立连接后,客户主机向服务端发送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)


    image.png
  • 异步I/O(POSIX的aio_系列函数)


    image.png

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 这些宏函数来设置或测试描述符集;

就绪条件

image.png

其中有数据可读或者可写,指接收缓存区或者发送缓存区数据字节数大于等于套接字缓存区低水位标记的大小,该标记可通过SO_RCVLOWATSO_SNDLOWAT选项修改;

连接关闭,指若为读关闭(接收了FIN的tcp连接),则读不会阻塞且返回0(即返回EOF);若为写关闭,则再写操作就会产生SIGPIPE信号;

注意若不是套接字描述符,如标准输入输出,则可读即内核缓存区存在数据可读,select不会感知用户缓存区的大小,因此,混合使用stdio与select需要小心,可参考select函数与stdio混用的不良后果

适用场景

  1. 多描述符情况下,可通过select判定哪些描述符可读写或异常,并且使用select不会存在无法感知对端异常崩溃或者连接关闭等情况(如使用其他阻塞调用未读写套接字,就会导致阻塞调用阻止获取对端异常情况);

  2. 使用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);
image.png

fcntl函数

与文件控制的名字相符,该函数执行各种描述符控制操作,如修改非阻塞I/O;


image.png

UDP 数据报传输协议

udp为无连接、无状态、不可靠的传输层协议,其中sokcet建立需要指定类型为SOCK_DGRAM;

常用的无连接udp传输函数流程如下:


image.png

具体的函数使用如下:

#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);

对于后两个参数,recvfromaccept类似,都是接收对端的地址;sendtoconnect类似,都是发送或者建立连接的地址;每个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

    image.png

    其他作用:

  • 再次调动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系统;

image.png

名字与地址转换

image.png

对于gethostbyname gethostbyaddr getservbyname getservbyport getaddrinfo getnameinfo等函数,是通过查询本地/etc/hosts或者使用/etc/resolv.conf配置文件获取dns服务器地址,进而通过udp查询相应的域名信息;

具体函数用途:

gethostbyname通过主机名查询ipv4地址;gethostbyaddr相反,通过ipv4地址查询主机名;

getservbyaddr通过服务名查询端口;getservbyport通过端口查询服务名;

getaddrinfogetnameinfo为协议无关转换函数,分别用于主机名字和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);

image.png

不同于其他套接字函数,该函数若失败,不会设置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);
image.png

获取的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套接字的网络客户端;

主要流程是:

  1. 系统启动后由launchd fork出syslogd,进程启动后读取/etc/syslog.conf /etc/asl.conf等配置文件;
  2. 创建`unix域套接字以监听系统日志相关的服务请求;
  3. 创建udp套接字,绑定固定的端口(bsd系统应该是514,mac不详);
  4. 打开路径名/dev/klog,内核的任何消息都是这个设备的输入;
image.png
注意:
  • syslog接口配置asl日志级别,需要修改/etc/asl.conf其中默认级别是notice
    image.png
  • 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

image.png

高级I/O函数

套接字I/O操作上设置超时时间的方法:

  • 调用alarm, 指定超时时间满后产生SIGALARM信号

    该方法对于多线程正常使用信号非常困难

  • select超时作为定时器超时阻塞;

  • 使用套接字选项SO_RCVTIMEO SO_SNDTMEOrecvfrom 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;

image.png

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 recvrecvfrom的通用I/O函数;

image.png

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不同的路径,但绑定也可以;

    注意事项
    1. bind绑定地址长度,可使用sizeof(struct sockaddr_un)结构体的长度,或者使用包含sun_path字符串长度,bind函数会自动获取su_path路径名,但必须保证输入长度不要超过结构体的长度,防止栈溢出

    2. bind绑定地址sun_path路径名可为空字符串作为抽象路径名,但Mac上不可行;

      若为空字符串,则等同于ipv4 INADDR_ANY或者ipv6 INADDR_ANY_INIT常值,不过依然存在路径名,只是不会创建该文件,具体可参考抽象 unix 域套接字地址

    3. 若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_STREAMSOCK_DGRAM,新创建的两个套接字描述符将作为sockfd[0]sockfd[1]返回。类似管道形式,但为流管道

传递描述符

当我们需要传递描述符时,通常可以使用方法有:

  1. fork调用返回以后,子进程共享父进程的所有描述符
  2. exec调用执行后,所有的描述符通常保持打开状态

第一种方式里,我们可以把描述符从父进程传递到子进程,然而我们也可能需要在子进程传递描述符到父进程。unix系统提供了用于从一个进程向其他任意进程传递描述符的方式,而这两个进程不需要有任何亲缘关系。这种技术要求在两个进程之间创建一个uds,然后使用sendmsg通过这个uds发送特殊结构的消息。这个特殊的消息会由内核处理,把打开的描述符从发送进程传递到接收进程。

通过uds传递描述符的步骤具体如下:

  1. 创建一个字节流或数据报的uds。这可以通过调用socketpair然后父子进程之间的连接;也可以使用套接字API。通常建议使用字节流套接字而不是数据报套接字,因为使用数据报套接字并没有什么好处,反而还存在数据报被丢弃的可能。
  2. 发送端打开描述符。uds可以传递各种类型的描述符,而不是仅包括文件描述符。
  3. 发送端进程创建一个msghdr的结构,其中含有待传递的描述符,然后调用sendmsg将其发送出去。发送一个描述符会使其引用计数加一。posix规定需要作为辅助数据发送描述符;
  4. 接收端进程调用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传递的另一种辅助数据就是用户凭证。用户凭证的数据结构在不同的操作系统中并不一致,这里就不再详细介绍了。不过需要使用定义的cmsgcred结构体传递凭证!

非阻塞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超时时间策略,可选择性重传丢失包,可调节延时发送ACKTCP可靠简单,但是复杂无私,所以速度慢。KCP尽可能保留UDP快的特点下,保证可靠。可靠UDP,KCP协议快在哪?

image.png

具体应用层使用:

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接收队列中的数据进行合并,并拷贝至用户缓存区;

image.png

image.png

源码解析参考:

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));

原始套接字输出

image.png

原始套接字还提供了一个非常有用的参数IP_HDRINCL:
1、当开启该参数时:我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有选项,但是IP报文头部中的标识字段(设置为0时)和IP首部校验和字段总是由内核自己维护的,不需要我们关心。
2、如果不开启该参数:我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段被设置成调用socket()函数时我们所传递给它的第三个参数。

原始套接字输入

image.png

调试技术

image.png

tcpdump wireshark抓包工具 抓取完整的数据,dtrace系统调用跟踪工具,netstat 查看哪些ip地址及端口在使用、tcp状态及路由信息等, lsof查看端口占用及所属进程、进程id、用户、描述符fd、类型、文件的大小或偏移、协议类型及名称等;
image.png

其它的如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已经链接了当前目录文件;

遇到的问题

  1. 函数定义冲突
    image.png

    屏蔽inet_ntop.c中的inet.h链接系统头文件,再次make编译;
  2. libxti not found
    编译说明只需要支持XTI系统才需要,暂不编译;
  3. Ld:symbol(s) not found for architecture x86_64
    image.png

    查看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

image.png

查看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链接时忽略了静态库!

image.png

网上遇到了类似问题:在macOS-Mojave上编译Lua失败的经历
image.png

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的链接路径;

image.png

具体问题:猜测是GNU的ar/ranlib 与 macos工具不一致导致

【解决】

修改src/Make.defines中的ranlib路径及lib/Makefile中的ar路径;


image.png
  1. connection refused
    image.png

    原因是获取时间的服务器(port:13)没有运行,intro/下有个daytimetcpsrv.c文件在另一个终端窗口下make后运行该服务器程序即可,如下;
    image.png

小知识

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_waitFIN_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

你可能感兴趣的:(unix网络编程)