《UNIX 网络编程 卷1:套接字联网API》第4章 基本TCP套接字编程
用于获得套接字描述符。
#include
int socket(int family, int type, int protocol);
返回:若成功则为非负描述符,若出错则为-1
函数中各个参数的含义如下:
并非所有套接字family与type的组合都是有效的,图1.4给出了一些有效的组合和对应的真正协议。其中标为“是”的项也是有效的,但还没有找到便捷的缩略词。而空白项则是无效组合。
图1.1 socket的family值:
family | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
图1.2 socket的type值
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEGPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
图1.3 socket函数的AF_INET或AF_INET6的protocol常值
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
图1.4 socket函数的family和type的组合
- | AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|---|---|---|---|---|
SOCK_STREAM | TCP|SCTP | TCP|SCTP | 是 | ||
SOCK_DGRAM | UDP | UDP | 是 | ||
SOCK_SEQPACKET | SCTP | SCTP | 是 | ||
SOCK_RAW | IPv4 | IPv6 | 是 | 是 |
socket函数成功返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符,简称sockfd。
AF_前缀表示地址族,PF_前缀表示协议族。在Linux系统中AF_XXX和PF_XXX是等价的。通常仅使用AF_常值。
客户端程序使用connect函数来建立与服务器的连接。
#include
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
返回:如成功则为0,若出错则为-1
函数中各个参数的含义如下:
1.客户端在调用connect前不必非得调用bind,内核会确定源IP地址,并选择一个临时端口作为源端口。
2.如果是TCP套接字,调用connect函数将触发TCP的3次握手过程,而且仅在连接建立成功或者出错时才返回。其中出错返回可能有以下几种情况:
(1)客户端没有收SYN分节的响应,则返回ETIMEDOUT错误。
备注:此处有定时器。以4.4BSD为例:发送SYN后,若无响应则等待6s后再次发送一个,若仍无响应则等待24s后再次发送一个。若总共等待了75s后未收到响应则返回本错误。
(2)若对客户端的SYN的响应是RST,则表明该服务器主机在我们指定的端口上没有进程在监听。这是一种硬错误(hard error),客户端一接收到RST就马上返回ECONNREFUSED错误。
(3)若客户发出的SYN在中间某个路由器上引发了一个“destination unreasonable”(目的地不可达)ICMP错误,则认为是一种软错误(soft error)。客户端主机内核保存该消息,并按第一种情况所描述的时间间隔继续发送SYN。若在某个规定的时间(4.4BSD规定75s)后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下两种情况也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;而是connect调用根本不等待就返回。
备注1:许多早期系统在收到ICMP错误时会不正确的放弃建立连接的尝试。说不正确是因为ICMP错误可能只是某个暂时状态,可修复。
备注2:网络不可达的错误被认为已过时,应用进程应该把EHOSTUNREACH和ENETUNREACH作为相同的错误对待。
bind函数把一个本地协议地址赋予一个套接字。
#include
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
返回:如成功则为0,若出错则为-1
函数中各个参数的含义如下:
图3.1给bind函数指定要捆绑的IP地址、端口号组合产生的结果
指定IP地址 | 指定端口号 | 结果 |
---|---|---|
通配地址 | 0 | 内核选择IP地址和端口 |
通配地址 | 非0 | 内核选择IP地址,进程指定端口 |
本地IP地址 | 0 | 进程指定IP地址,内核选择端口 |
本地IP地址 | 非0 | 进程指定IP地址和端口 |
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。
如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
对于IPv4,IP地址是一个32位的值,通配地址由常值INADDR_ANY来指定,其值一般为0。使用方式如下:
struct sockaddr_in serverAddr
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
对于IPv6,IP地址是一个128位的结构体,不能用常量表示。系统预先分配in6addr_any变量并将其初始化为IN6ADDR_ANY_INIT。头文件
struct sockaddr_in6 serverAddr
serverAddr.sin_family = AF_INET6;
serverAddr.sin_addr.s_addr = in6addr_any;
无论是网络字节序还是主机字节序,INADDR_ANY的值(为0)都一样,因此使用htonl并非必需。不过既然头文件
listen函数仅由TCP服务器调用,它做两件事:
(1)当socket函数创建一个套接字时,它被认为一个主动套接字(将调用connect发起连接的客户端套接字)。listen函数把它转换成一个被动套接字,指示内核应该接受指向该套接字的连接请求。根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换到LISTEN状态。
(2)本函数的第二个参数指定了内核应该为相应套接字排队的最大连接个数。
#include
int listen(int sockfd, int backlog);
返回:如成功则为0,若出错则为-1
函数中各个参数的含义如下:
其中backlog参数较为复杂,我们先来了解一些基础知识。内核为任何一个监听套接字维护两个队列:
(1)未完成连接队列(incomplete connection queue),已收到SYN分节,而服务器正在等待完成3次握手过程。这些套接字处于SYN_RCVD状态。
(2)已完成连接队列(completed connection queue),已完成3次握手过程。这些套接字处于ESTABLISHED状态。
listen函数的backlog参数曾被定义为这两个队列总和的最大值,但不同系统实现时都有不同算法,通常允许的实际最大值都比这个参数要大(+1,+2或者*1.5等不同实现方式)。
备注:当一个客户端SYN到达时,若这些队列是满的,TCP就忽略该分节,也不会发送RST。这么做是因为:这种情况是暂时的,客户端将重发SYN,期望不久就能在队列中找到可用空间。要是服务器TCP立即响应一个RST,客户端的connect调用就会立即返回一个错误,强制应用程序处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户端无法区分响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”。
accept函数由TCP服务器调用,用于从已完成连接队列的队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。
#include
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t* addrlen);
返回:如成功则为非负描述符,若出错则为-1
函数中各个参数的含义如下:
sockfd
由socket函数返回的套接字描述符
cliaddr
用来返回已连接的对端进程的协议地址
addrlen
值-结果参数,cliaddr结构体长度
本函数最多返回3个值:return值,cliaddr,addrlen。如果对客户端的协议地址不感兴趣,那么可以把cliaddr和addrlen均置为空指针。
用来关闭套接字,并终止TCP连接。
#include
int close(int sockfd);
返回:如成功则为0,若出错则为-1
函数中各个参数的含义如下:
close一个套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。
TCP在close后将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列(4次通信)。
SO_LINGER套接字选项可以用来改变TCP套接字的这种默认行为,此参数的具体用法我会在其他文章中说明。
每个文件或套接字都有一个引用计数。引用计数在文件表项中维护,它是当前打开着的引用该文件或套接字的描述符的个数。当调用fork后,父进程的描述符会在父子进程中都存在,描述符引用计数将会变成2。只有2个进程都调用了close后,描述符的引用计数才会减为0。