套接字地址结构
IPv4套接字地址结构,定义如下,位于/usr/include/netinet/in.h:
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
在使用结构前总是把整个结构置零,是由于sin_zero字段没有任何作用,只需置零即可。
为了是套接字函数在处理地址时具有一定的通用性,套接字函数还定义了通用套接字地址结构,定义位于/usr/include/x86_64-linux-gnu/bits/socket.h
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
通过上述定义可以发现通用地址结构就是通过字符数组进行占位,但对于其中的数据类型并没有进行规定。
由于网络中的计算机体系结构不同,这些不同的体系结构之间一个比较明显的区别就在于其“字节序”不同,有关于字节序的详细内容在此也就不深入分析了。但不同的机器之间字节序不同,造成了机器之间信息不能直接传输。其实不同字节序的机器的数据转换也很简单,只要根据不同的类型逐字节反转就行了。不过这种转换函数库函数已经帮我们实现好了,定义如下,还是位于/usr/include/netinet/in.h中
extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
extern uint16_t ntohs (uint16_t __netshort)
__THROW __attribute__ ((__const__));
extern uint32_t htonl (uint32_t __hostlong)
__THROW __attribute__ ((__const__));
extern uint16_t htons (uint16_t __hostshort)
__THROW __attribute__ ((__const__));
让我们仔细分析一下这几个函数,其中n(network)代表网络字节序(大端),h(host)代表主机字节序(与机器具体情况相关)。l代表long(例如IPv4地址),s代表short(例如TCP、UDP的端口号)。以ntohl为例,这个函数的功能就是将IPv4(此处以IPv4地址为例)由网络字节序转换为主机字节序。
在套接字地址结构中存放的地址是uint_32类型的,但我们常用的地址描述方式是点分十进制,因此套接字给出了在点分十进制与套接字地址结构之间的转换函数。
#include <arpa/inet.h>
extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW; //成功返回1,失败返回0
extern in_addr_t inet_addr (const char *__cp) __THROW; //返回值为32位的网络字节序二进制值
以上两个函数用于将点分十进制字符串转换为套接字地址结构。
同样存在功能相反的函数,即将网络字节序转换为点分十进制字符串
#include <arpa/inet.h>
extern char *inet_ntoa (struct in_addr __in) __THROW;
上述三个函数仅适用于转换IPv4地址,并不适用于IPv6地址,因此套接字函数定义了更加通用的函数。
#include <arpa/inet.h>
extern int inet_pton (int __af, const char *__restrict __cp,
void *__restrict __buf) __THROW;
extern const char *inet_ntop (int __af, const void *__restrict __cp,
char *__restrict __buf, socklen_t __len)
__THROW;
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数。
#include <sys/socket.h>
extern int socket (int __domain, int __type, int __protocol) __THROW;
其中__domin代表协议族,可能的取值有AF_INET(IPv4协议族),AF_INET6(IPv6协议族)。
__type代表套接字类型,可能的取值有SOCK_STREAM(字节流套接字),SOCK_DGRAM(数据报套接字)。
__protocol应设为某个类型常值,或直接设为0,以选择所给定的__domain与__type组合的系统默认值。
这里还要提一点的就是在我的机器上AF_XXX与PF_XXX定义是相同的,相关定义位于/usr/include/x86_64-linux-gnu/bits/socket.h文件中。
TCP客户用connect函数来建立与TCP服务器的连接。调用connect函数将激发TCP的三路握手过程。
#include <sys/socket.h>
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
其中出错返回可能有以下几种情况
- 若TCP没有收到SYN分节的响应,则返回ETIMEDOUT错误。
- 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接。这是一种硬错误(hard error)。客户一收到RST就马上返回ECONNREFUSED错误。
- 若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误。这是一种软错误(soft error)。客户主机内核先保存该错误,并等待一段时间后继续发送SYN,若在某个规定的时间后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。在每次connect函数失败后,都必须close当前的套接字描述符并重新调用socket。
bind函数把一个本地协议地址赋予一个套接字。
#include <sys/socket.h>
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
__THROW;
若TCP客户调用bind函数,就为在该套接字上发送的IP数据报指派了源IP地址。
若TCP服务调用bind函数,就限定该套接字只接收那些目的地为这个IP地址的客户连接。
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。
如果指定IP地址为通配地址,那么内核将等到套接字已连接或已在套接字上发出数据报时才选择一个本地IP地址。
listen函数仅用于TCP服务器调用,listen函数把一个未连接套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求,调用listen导致套接字从CLOSED转换为listen状态。
函数原型如下:
#include <sys/socket.h>
extern int listen (int __fd, int __n) __THROW;
内核为一个给定的监听套接字维护两个队列:
未完成队列:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程,这些套接字处于SYN_RECV状态。
已完成队列:每个已完成的TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
以上两个队列的和不超过参数_n的和。
accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成连接。函数原型如下:
#include <sys/socket.h>
extern int accept (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
若accept函数成功,则返回一个全新的描述符,这一描述符被称为已连接套接字。而accept函数中的__fd被称为监听套接字。
这里对于accept函数还有一个概念——“呼入连接请求队列”。在服务器端中有一个固定长度的连接队列,该队列中的连接已被TCP接受(即三次握手已经完成),但还没有被应用层接受,这个队列就是“呼入连接请求队列”。注意区分TCP接受一个连接是将其放入这个队列,而应用层接受连接是将其从该队列中移出。应用层将指明该队列的最大长度,这个通常被称为积压值(backlog)。
对于新的连接请求,该TCP监听的端点的连接队列中还有空间,TCP模块将对该SYN进行确认并完成连接的建立。
close函数用于关闭套接字,并终止TCP连接。
#include <unistd.h>
extern int close (int __fd);
对于并发服务器而言,frok返回后在进程的地址空间中同时存在两个套接字,分别是监听套接字与已连接套接字,这两个套接字中已连接套接字一定处于ESTABLISHED状态,但监听套接字的状态不是非常确定。父进程首先会关闭已连接套接字,但close函数只是使描述符引用计数减1,但此时子进程的已连接套接字并未关闭,因此不会引发TCP的连接终止序列。监听套接字的情况类似。