基本的套接字函数
文章目录
socket函数
概要
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()
创建一个用于通讯的端点,返回一个文件描述符。
domain参数
domain
参数指定一个地址族。下面是一些常见的地址族:
地址族 | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | 本地通讯 |
type参数
type
参数指定套接字类型。下面是一些常见的类型:
类型 | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
对于某个地址族,不一定实现了所有的套接字类型。
从Linux 2.6.27开始,type
参数的存在有两个目的:除了指定套接字类型外,它也可以包含任何以下值的位或,以修改socket()
的行为:
- SOCK_NONBLOCK:在新的打开的文件描述符上设置O_NONBLOCK文件状态标志。
- SOCK_CLOEXEC:在新的打开的文件描述符上设置FD_CLOEXEC标志。
protocol参数
protocol
参数指定一个特定的协议(协议号)。通常,在给定的地址族中,只有一个协议可以支持特定的套接字类型,在这种情况下,protocol
可以指定为0(大部分情况下都是0,以选择domain
和type
组合的系统默认值)。
在Linux中,通过/etc/protocols文件查看协议的定义。通过getprotoent
函数将一个协议名映射到协议号。
返回值
socket()
在成功时返回一个新的套接字的文件描述符,错误时返回-1并设置errno
。
domain和type常见的组合
下面是一些domain
和type
常见的组合:
AF_INET | AF_INET6 | |
---|---|---|
SOCK_STREAM | TCP | SCTP | TCP | SCTP |
SOCK_DGRAM | UDP | UDP |
SOCK_SEQPACKET | SCTP | SCTP |
SOCK_RAW | IPv4 | IPv6 |
对比AF_XXX和PF_XXX(地址族和协议族)
AF_前缀表示地址族,PF_前缀表示协议族。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_值用来创建套接字,而AF_值用于套接字地址结构。但实际上,支持多个地址族的协议族从来就未实现过,而且头文件<sys/socket.h>中为一给定协议定义的PF_值总是与此协议的AF_值相等。尽管这种相等关系并不一定永远成立,但若有人试图给已有的协议改变这种约定,则许多现存代码都将崩溃。
在4.x BSD下的协议族常量是PF_UNIX,PF_INET等,而AF_UNIX,AF_INET等被用于地址族。然而,已有的BSD手册承诺:协议族通常与地址族相同,并且以后的标准都使用AF_XXX。
bind函数
概要
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
bind
函数把一个本地协议地址赋予一个套接字。对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
addr和addrlen参数
当使用socket()
创建一个套接字时,它存在于一个命名空间(地址族)中但是没有分配地址给它。bind()
通过addr
分配地址给sockfd
引用的套接字,addrlen
指定地址结构的大小。
addr
的实际结构取决于地址族,把它看成const void*
类型即可。
调用bind
函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可都不指定:
如果指定端口号为0,那么内核就在bind
被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0。它告知内核去选择IP地址。
对于IPv6来说,通配地址由变量in6addr_any来指定。
返回值
bind
函数在成功时返回0,错误时返回-1并设置errno
。
connect函数
概要
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
TCP客户用connect
函数来建立与TCP服务器的连接。
sockfd
是由socket
函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有服务器的IP地址和端口号。
客户在调用函数connect
前不必非得调用bind
函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect
函数将发送SYN分节并激发TCP的三路握手过程,而且仅在连接建立成功(收到服务器的SYN和附带的ACK分节)或出错时才返回,其中出错返回可能有以下几种情况:
-
若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。举例来说,调用connect函数时,4.4BSD内核发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个。若总共等了75s后仍未收到响应则返回本错误。
-
若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想取消一个已有连接;TCP接收到一个根本不存在的连接上的分节。
-
若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该消息,并按第一种情况中所述的时间间隔继续发送SYN。若在某个规定的时间(4.4BSD规定75s)后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETYUNREACH错误返回给进程。以下两种情形也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;二是connect调用根本不等待就返回。
按照TCP状态转换图,connect
函数导致当前套接字从CLOSED状态(该套接字自从由socket
函数创建以来一直所处的状态)转移到SYN_SENT状态,若成功则再转移到ESTABLISHED状态。若connect
失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect
函数。
addr和addrlen参数
这两个参数与上面的bind
函数中的两个参数参数类似,实际结构取决于地址族。
返回值
connect
函数在成功时返回0,错误时返回-1并设置errno
。
listen函数
概要
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen
函数仅由TCP服务器调用,它做两件事情:
- 当
socket
函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect
发起连接的客户套接字。listen
函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。根据TCP状态转换图,调用listen
导致套接字从CLOSED状态转换到LISTEN状态。 listen
函数第二个参数规定了内核应该为相应套接字排队的最大连接个数。
这个函数通常应该在调用socket
和bind
这两个函数之后,并在调用accept
函数之前调用。
backlog参数
为了理解其中的backlog
参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
- 已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
上面两幅图来自《UNIX网络编程》。
在Linux中,如果backlog
参数大于/proc/sys/net/core/somaxconn中的值,那么将被截断为该值。从Linux5.4开始,这个文件中的默认值是4096;在早期的内核中,默认值是128。内核早于2.5.25之前,限制为一个硬编码的值SOMAXCONN,即128。如果连接请求抵达时队列已满,客户将收到一个ECONNREFUSED错误,或者如果底层协议支持重传,请求可能会被忽略,以便之后的尝试将成功连接。
在Linux中,backlog
参数在TCP套接字上的行为在Linux 2.2中发生改变。现在它指的是完全建立连接并等待accept
的套接字的数量,而不是未完成连接的请求的数量。
返回值
listen
函数在成功时返回0,错误时返回-1并设置errno
。
accept函数
概要
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被阻塞(假定套接字为默认的阻塞方式)。
addr和addrlen参数
参数addr
和addrlen
用来返回已连接的对端进程(客户)的协议地址。为了能处理IPv6地址,应将更大的struct sockaddr_storage
赋予给addr
。和往常一样,我们应该将struct sockaddr *
看成void*
,因为实际的地址结构是未知的。
addrlen
是值-结果参数,调用前,我们将*addr
所占的字节数传递给addrlen
,返回时addrlen
被赋值为addr
实际地址结构的字节数。
返回值
accept
函数在成功时返回一个非负的整数值,它是被接受套接字的文件描述符,在错误时返回-1并设置errno
,且addrlen
保持不变。
close函数
概要
#include <unistd.h>
int close(int fd);
close
函数可以用来关闭套接字,并终止TCP连接。
被关闭的套接字描述符不能再作为read
和write
的第一个参数,然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列(四次挥手)。
fd参数
fd
是一个文件描述符,可以为一个套接字描述符。
返回值
close
函数在成功时返回0,错误时返回-1并设置errno
。
描述符引用计数
close
已连接的描述符将导致该描述符的引用计数减1,当引用计数减为0时,套接字才被关闭,对于TCP套接字,这将发送FIN分节并引发四分组连接终止序列(四次挥手)。
shutdown函数
概要
#include <sys/socket.h>
int shutdown(int sockfd, int how);
终止网络连接的通常方法是调用close
函数。不过close
有两个限制,却可以使用shutdown
来避免:
close
把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用shutdown
可以不管引用计数就激发TCP的正常连接终止序列。close
终止读和写两个方向的数据传送。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。
how参数
how
参数可以为以下3个值:
- SHUT_RD:关闭连接的读这一半,套接字中不再有数据可接收,且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用
shutdown
函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。 - SHUT_WR:关闭连接的写这一半(对于TCP套接字将发送FIN分节),对于TCP套接字,这称为半关闭(half-close)。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。不管套接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。
- SHUT_RDWR:连接的读半部和写半部都关闭,这与调用
shutdown
两次等效,第一次调用指定SHUT_RD,第二次调用指定SHUT__WR。
返回值
shutdown
函数在成功时返回0,错误时返回-1并设置errno
。
参考
《UNIX网络编程》
socket(2)
bind(2)
connect(2)
listen(2)
accept(2)
close(2)
shutdown(2)