套接字描述符
套接字是通信端点的抽象,是软件接口,是传输层和网络层之间的接口。正如使用文件描述符访文件,使用套接字描述符访问套接字。套接字描述符本质上就是一个文件描述符,但是不是所有参数为文件描述符的函数都可以接收套接字描述符(比如说套接字不支持文件偏移量,所以lseek等函数就不支持)。
寻址
字节序
大端:最大字节出现在最低地址位。
小端:最小字节出现在最低地址位。
TCP/IP协议栈使用大端字节序,Linux x86使用小端。
四个字节序转换函数:
h表示host,n表示network,l表示long(32位整数), s表示short(16位整数)
- htonl
- htons
- ntohl
- ntohs
点分十进制和二进制转换
inet_pton
inet_ntop
bind
bind一般是在服务器端需要调用,要指定周知端口号(01023),一般服务器的IP地址设置为INADDR。而客户端一般是由内核指定一个端口号,这个端口号是动态或者私用的(4915265536)。还有中间的已登记端口号。
如果地址已经被使用,就会返回EADDRINUSE,可以使用SO_REUSEADD和SO_REUSEPORT。
listen
listen,服务器执行被动打开(LISTEN)。listen的第二个参数是已完成的等待被accept的最大数量(不知道怎么回事,我在linux测试没有成功)。
SYN泛洪攻击。
connect
connect,调用时从CLOSED转到SYN_SENT(从套接字创建以来是CLOSED),如果返回成功是ESTABLISHED。如果connect失败,这个套接字不可用,需要close它,然后重新打开。
connect出错返回的三个可能:
- TCP客户没有收到服务器对SYN的回应。返回ETIMEOUT。
- 如果对于客户的相应是RST,表示服务器主机在指定的端口没有进程等待和它连接。这是一种硬错误,客户一接收到RST报文就立刻返回ECONNREFUSED错误。(产生RST的三个条件:TCP想取消一个连接;TCP接收到一个根本不存在的连接上的报文,还有就是SYN报文段到了目的地,但是没有正在监听的服务器)。
- 如果客户发出的SYN在中间某个路由器引发了目的地不可达ICMP错误,就认为是一种软错误。
accept
accept从已完成连接的队列取一个连接回来。可能导致accept返回错误的情况:
- 被某个信号中断了,某些系统设置了被中断后的自动恢复,但是有些没有,所以,要在accept内进行中断的处理。
- accept返回之前连接终止。大多数情况下返回一个错误,POSIX要求返回的错误是ECONNABORTED。遇到这个错误,服务器可以忽略它,再次调用accept。
刚开始,一直以为accept返回的一个套接字描述符,使用了新的IP地址,因为使用了fork。后来才发现自己好傻逼,fork是创建了一个子进程,有一个新的pid,而并不影响这个套接字的四元组。。
这也是为什么如果主机重启之后,对面会发送一个RST,因为你连接的还是那个端口号,只不过是刚才的那个连接已经断开了,这就是发送一个RST的三个条件之一。
close
close关闭一个套接字,默认行为是把该套接字标记成关闭,然后立即返回到调用进程,该套接字不能在由调用进程使用,也就是说他不能作为read和write的第一个参数。但是TCP将尝试发送已经排队等待发送到对端的任何数据,发送完毕后执行正常的四次挥手。
服务器进程中止连接
如果客户端和服务器连接,杀死服务器子进程。这时,服务器子进程调用exit,关闭所有文件描述符,导致服务器子进程向客户端发送一个FIN报文。然后客户端就处于CLOSE_WAIT状态了。
如果客户端继续向服务器发送报文,是允许的。这个时候TCP会返回一个RST(因为这个时候TCP收到了一个根本不存在的连接上的报文)。这个RST会不会被客户端看到取决于接下来用户从套接字读时,取决于客户端先收到由于FIN产生的EOF还是RST产生的。
如果进程第二次写,也就是收到了RST之后,内核会向这个进程发送一个SIGPIPE信号。SIGPIPE的默认行为是终止进程,所以进程为了不让程序崩溃,必须捕获它。无论捕获的操作是设置一个信号处理程序还是简单的忽略,写操作都会收到一个EPIPE错误。
服务器主机奔溃
假设主机崩溃,已有的网络连接上发不出任何东西。
- 客户端TCP发送数据,这个时候,客户端TCP一直重传。如果服务器崩溃,对客户的数据没有任何影响,返回的错误是ETIMEOUT。如果中间的某个路由器判断服务器不可能,返回的是EHOSTUNREACH或者ENETUNREACH。
- 如果客户端TCP没有发送数据。需要使用套接字的SO_KEEPALIVE选项。
服务器主机奔溃后重启
主机崩溃了,然后重启,即客户端完全不知道服务端出过事情。
如果客户端不给服务端发送数据,客户就不知道服务器崩溃。
如果客户发送了数据,这时服务器TCP会返回一个RST,因为服务器丢失了崩溃前的所有连接,就会返回ECONNRESET。
服务器关机
和服务器进程被终止是一样的,因为服务器子进程被关闭,文件描述符被关闭。
套接字选项
- SO_KEEPALIVE选项。如果开启的话,在两小时之内,套接字的任何一个方向上都没有数据交换,TCP会自动给对端发送一个保持存活探测报文,这是对端必须响应的一个TCP报文,结果有三种可能:收到对方的ACK,对方返回一个RST,或者无响应。
一般这个选项用在服务器端,当客户机掉线之后,关闭这种半开连接。 - SO_LINGER选项。指定close函数对面向连接的协议的工作。默认的close操作是立即返回,但是如果有数据残留在套接字发送缓冲区,系统会试着把这些数据发送给对端。
- SO_RCVBUF和SO_SNDBUF选项。每一个套接字都有一个发送缓冲区和接收缓冲区。接收缓冲区中可用空间的大小先顶了TCP通告对端窗口的大小。
对于客户而言,SO_RCVBUF必须在调用connect之前设置,而对于服务器来说,SO_RCVBUF必须在listen前设置。TCP套接字的缓冲区的大小至少应该是相应连接的MSS的4倍。
UDP没有发送缓冲区只有发送缓冲区大小,这个发送缓冲区大小表示的是能写到套接字的最大UDP数据报大小。 - SO_REUSEADDR。四个作用:
- 监听服务断开,而子进程还在服务着。这个时候如果重启服务器,因为端口号被子进程占用着,所以就会出错。
- 同一端口启动多个实例。对于TCP,允许在同一个端口上启动绑定不同IP的同一服务器的多个实例。但是不允许启动绑定同一个端口和IP的多实例。
- 同一端口,不同IP,绑定多个socket。允许单个进程将同一个端口绑定到多个socket上,只要每次捆绑指定不同的IP地址即可。
- 对于UDP,允许完全重复的捆绑,一个IP地址和端口绑定到一个socket上时,同样的IP地址和端口还可以绑定到另一个socket上。这应该是UDP本身的设计吧,因为UDP的套接字就是通过端口进行区分的。
- SO_REUSEPORT选项。如果bind一个
shutdonw和close
参考文献
1.《APUE》
2.《UNP》卷一