#include<sys/socket.h>
int socket(int family, int type,int protocol);
返回:若成功则为非负描述符,若出错则为-1
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传输协议
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen);
返回:若成功则为0,若出错则为-1
客户在调用函数connect之前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况。
- 若TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误。即连接超时。
- 若对客户的SYN响应是RST(表示复位),则表明该服务器主机在我们指定的端口没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误,客户一接收到RST就马上返回ECONNREFUSED错误。 RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。
- 若对客户发出的SYN在中间的某个路由上引发一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误。客户主机内核保存该信息,并按第一种情况中所述的时间间隔继续发送SYN。若在某个规定的时间(4.4BSD规定75s)后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下两种情况也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;二是connect调用根本不等待就返回。
非阻塞套接字connect:此时无论是否成功connect都立即返回,如果返回-1,错误码为EINPROGRESS,则表示正在尝试连接,否则表示出错。在Windows上可以调用select函数,在指定时间内判断该socket是否可写,如果可写,则说明连接成功,反之认为连接失败。在Linux上不仅要调用select检测是否可写,还要调用getsockopt检测此时socket是否出错,通过错误码来检测和确定是否连接上,错误码为0时表示连接上,反之则是未连接上。
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
返回:若成功则为0,若出错则为-1
通配地址由常值INADDR_ANY来指定,其值一般为0。
对于TCP调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。
如果一个TCP客户或服务器未曾调用bind捆绑一个端口,那么当调用connect或linsten时,内核就要为套接字选择一个临时端口。
从bind函数返回的一个常见错误是EADDRINUSE(地址已使用)。
int listen(int sockfd, int backlog);
返回:若成功则为0,若出错则为-1
内核为任何一个给定的监听套接字维护两个队列:
(1)未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程,这些套接字都处于SYN_RCVD状态。
(2)已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态。
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。
当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中捎带对客户的SYN的ACK。这一项一直保存在未连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。(源自Berkeley的实现为这些未完成连接的项设置的超时值为75s)如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时( 该函数在下一节讲解),已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被置于休眠状态,直到TCP在该队列中放入一项才唤醒它。
当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。
在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回:若成功则为非负描述符,若出错则为-1
如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。在讨论accept函数时,称它的第一个参数为监听套接字描述符,它的返回值为已连接套接字描述符。
如果我们对返回的客户协议地址不感兴趣,那么可以把cliaddr和addrlen均置为空指针。
int recv(int sockfd, void* buf, int len,int flags);
返回值:若成功返回接收的字节数,若出错返回-1,连接断开返回0
返回值等于0:收到对端关闭的FIN包
大于0:接收成功的字节数。
等于-1:errno=EWOULDBLOCK 非阻塞IO,读缓冲为空
errno=EINTR 系统调用被中断打断,重新接收即可
errno=ETIMEDOUT tcp探活包超时
int send(int sockfd, void *buf, int len, int flags);
返回值:成功返回接收的字节数,出错返回-1,连接断开返回0
返回值等于0:收到对端关闭的FIN包
大于0:发送成功的字节数。
等于-1:errno=EWOULDBLOCK 非阻塞IO,写缓冲满
errno=EINTR 系统调用被中断打断,重新发送即可
errno=EPIPE 写通道关闭
注意:send发送0字节时也会返回0,此时不会向对端发送任何数据,而recv只有在对端关闭连接时才会返回0,对端发送0字节数据,本端的recv函数是不会接收到0字节数据的。
int close(int sockfd);
返回:若成功则为0,若出错则为-1
close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列(2.6节)。
并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。既然引用计数值仍大于0,这个close调用并不引发TCP的四分组连接终止序列。对于父进程与子进程共享已连接套接字的并发服务器来说,这正是所期望的。
如果我们确实想在某个TCP连接上发送一个FIN, 那么可以改用shutdown函数(6.6节)以代替close。
int getsockname(int sockfd,struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr, socklen_t *addrlen);
均返回:若成功则为0,若出错则为-1
这两个函数的最后一个参数都是值-结果参数。
这两个函数都得装填由localaddr或peeraddr指针所指的套接字地址结构。
使用这两个函数的理由如下:
(1)在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。
(2)在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号。
(3)getsockname可用于获取某个套接字的地址族。
(4)在一个以通配IP地址调用bind的TCP服务器上, 与某个客户的连接一旦建立(accept成功返回),getsockname就可 以用于返回由内核赋子该连接的本地IP地址。在这样的调用中,套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符。
(5)当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途 径便是调用getpeername。
处理被中断的系统调用
术语“慢系统调用”:是可能永远阻塞的系统调用。
适用于慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核会自动重启某些被中断的系统调用。不过为了移植,应该主动捕获错误重启该系统调用。如accept()函数。
accept返回前连接中止**
这里,三路握手完成从而连接建立之后,客户TCP却发送了一个RST(复位)。在服务端看来,就在该连接已由TCP排队,等着服务器进程调用accept的时候RST到达。稍后,服务器进程调用accept。
处理这种中止的连接依赖于不同的实现。源自于Berkeley的实现完全在内核中处理中止的连接,服务器进程根本看不到。然而大多数SVR4实现返回一个错误给服务器进程,作为accept的返回结果,不过错误本身取决于实现。这些SVR4实现返回一个EPROTO(“protocol error”,协议错误)errno值,而POSIX指出返回的errno值必须是ECONNABORTED。POSIX做出的修改的理由在于:流子系统中发生某些致命的协议相关事件时,也会返回EPROTO。要是对于由客户引起的一个已建立连接的非致命中止也返回同样的错误,那么服务器就不知道该再次调用accept还是不该了。 换成ECONNABORTED错误,服务器就可以忽略它,再次调用accept就可以了。
服务器进程中止
客户端已经连接服务端后,服务器进程中止,客户TCP接收来自服务器TCP的FIN并响应以一个ACK。但是客户进程看不到这个RST,因为它调用writen后马上readline,并由于接收到的FIN,所调用的readline立即返回0(表示EOF)。
当一个进程向某个已收到RST套接字执行写操作时,内核向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿地被终止。
不论进程是捕获了该信号并从其信号处理函数返回,还是简单地忽略该信号,写操作都将返回EPIPE错误。
第一次写操作会引发RST,第二次写会引发SIGPIPE信号。写一个已接收了FIN的套接字不成问题,但是写一个已接收了RST的套接字则是一个错误,会将引发SIGPIPE信号杀死自身进程。
服务器主机崩溃
当服务器主机崩溃时,已有的网络连接上不发出任何东西。这里假设的是主机崩溃,而不是由操作员执行命令关机。
此时我们在客户上键入一行文本,它由writen 写入内核,再由客户TCP作为个数据分节送出。客户随后阻塞于readline调用,等待回射的应答。
如果我们用tcpdump观察网络就会发现,客户TCP持续重传数据分 节,试图从服务器上接收一个ACK。TCPv2的25.11节给出了TCP重传的一个典型模式:源自Berkeley的实现重传该数据分节12次,共等待约9分钟才放弃重传。当客户TCP最后终于放弃时(假设在这段时间内,服务器主机没有重新启动,或者如果是服务器主机未崩溃但是从网络上不可达,那么假设主机仍然不可达),给客户进程返回一个错误。既然客户阻塞在readline调用上,该调用将返回一个错误。假设服务器主机已崩溃,从而对客户的数据分节根本没有响应,那么所返回的错误是ETIMEDOUT。然而如果某个中间路由器判定服务器主机已不可达,从而响应以一个“destinationunreachable" (目的地不可达) ICMP消息,那么所返回的错误是EHOSTUNREACH或ENETUNREACHD尽管我们的客户最终还是会发现对端主机已崩溃或不可达,不过有时候我们需要比不得不等待9分钟更快地检测出这种情况。所用方法就是对readline调用设置一个超时。
我们刚刚讨论的情形只有在我们向服务器主机发送数据时才能检测出它已经崩溃。如果我们不主动向它发送数据也想检测出服务器主机的崩溃,那么需要采用另外一个技术, 也就是我们将在7.5节讨论的KEEPALIVE套接字选项。
服务器主机崩溃后重启
我们先在客户和服务器之间建立连接,然后假设服务器崩溃重启。
当服务器崩溃重启时,它的TCP丢失崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST。
当客户TCP收到该RST,当客户正阻塞于readline调用,导致该调用返回ECONNRESET错误。
服务器主机关机
Unix系统关机时,init进程通常会给所有进程发送SIGTERM信号(该信号可被捕获),等待一段固定时间(往往在5-20秒),然后给所有仍在运行的进程发送SIGKILL信号(该信号不能被捕获)。这么做是留给所有运行的进程一小段时间来清除和终止。如果我们不捕获SIGTERM信号并终止,我们的服务器将由SIGKILL信号终止。当服务器子进程终止时,它的所有打开的描述符都被关闭,随后的发生的步骤和服务器进程终止的一样。
数据格式
在客户和服务器之间传输文本串,不用考虑客户与主机的字节序如何。
在客户与服务器之间传递二进制结构时,需要考虑客户与服务器之间的字节序问题。
解决这种数据格式问题有两种解决办法:
(1)把所有的数值数据作为文本串来传递。需要假设客户和服务器具有相同的字符集。
(2)显示定义所支持的数据类型的二进制格式(位数、大端或小端字节序),并以这样的格式在客户和服务器之间传递所有数据。
来自Unix网络编程卷一