套接字
1. 套接字Socket
通信的本质是两机器上两进程间的通信。
A network socket is a software structure that serves as an endpoint for sending and receiving data across the network. (对通信端点的抽象)
以下两张图,可以辅助来理解套接字。
如下图,网络协议栈类似于一个排插,两个套接字在两端插上去,就可使用协议栈进行通信。
2. 套接字编程模型
2.1 TCP
2.2 UDP
3. 服务端操作
以TCP套接字编程模型的服务端为例,按步骤介绍涉及到的API。
3.1 创建套接字
使用函数socket创建套接字,
int socket(int domain, int type, int protocol);//成功返回套接字描述符.出错返回-1
这一步事实上是确定通信特征,各个域domain
有自己的格式表示地址,以AF_
开头(address family);type
确定套接字类型,如数据报、字节流;协议protocol
对同一个域和套接字类型支持的多个协议进行选择,通常为0
,即按给定的域和套接字类型选择默认的协议。典型的TCP、UDP如下:
TCP:(AF_INET, SOCK_STREAM, 0)
UDP:(AF_INET, SOCK_DGRAM, 0)
注:尽管套接字本质是文件描述符,但不是所有用于文件操作的函数都能用于套接字操作,比如lseek
,套接字不支持文件偏移量。
3.2 绑定
使用函数bind进行套接字绑定,
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);//成功返回0.出错返回-1
bind
函数用于将地址绑定到一个套接字。服务器需要给一个接收客户端请求套接字绑定一个众所周知的地址,而客户端可以让系统选一个默认地址绑定(无须绑定)。
(1) 套接字地址sockaddr_in
在IPv4因特网域AF_INET
中,套接字地址用结构sockaddr_in
表示,如下:
struct sockaddr_in
{
sa_family_t sin_family; //unsigned short 地址族
in_port_t sin_sport; //uint16_t
struct in_addr sin_addr; //IPv4
};
struct in_addr
{
in_addr_t s_addr; //uint32_t
};
注:
初始化sockaddr_in
结构体时,因为sin_port
和sin_addr
被封装在网络传输,所以端口号和地址必须用网络字节序;而sin_family
只是被内核用来决定数据结构包含什么类型的地址,没有发送到到网络,应该是本机字节顺序。处理器与网络字节序之间转换函数为htonl
、htons
、ntohl
、ntohs
(h
指host
主机,n
指network
网络,l
指long
32位,s
指short
16位)。
理论上,端口号可以是0~65535
,但1~1023
已由IANA管理,绑定时端口号不少于1024。
此处的地址s_addr
是二进制地址格式,如果参数是点分十进制字符串表示,则需通过函数inet_ntop
(将网络字节序的二进制地址转换成点分十进制字符串表示)、inet_pton
进行相互转换。其转换过程如下:
127.0.0.1 --> 7F.0.0.1 --> 100007F=16777343(网络字节序为大端)
如果地址s_addr
为INADDR_ANY
,套接字端点可以被绑定到所有系统网络接口,即可以收到这个系统所安装的所有网卡的数据包。
(2) 通用地址格式sockaddr
地址格式与特定的通信域有关(如AF_INET
、AF_INET6
),为使不同地址格式地址能够传入套接字函数,地址被强制转换成通用的地址结构sockaddr
,如下(以Linux为例):
struct sockaddr
{
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
3.3 监听listen
使用函数listen进行监听,
int listen(int sockfd, int backlog);//成功返回0,出错返回-1
一旦服务器调用listen
,套接字就能接收连接请求。backlog
用于表示该进程所要入队的连接请求数量,实际值由系统决定,但上限由SOMAXCONN
指定。一旦队列满,系统会拒绝多余连接请求。
3.4 接受连接请求accept
使用函数accept接受连接请求,
//成功返回套接字描述符,出错返回-1
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
使用accept
获得连接请求并建立连接,新的套接字描述符连接到调用connect的客户端。传给accept
的原始套接字(sockfd
)没有关联到这个连接,而是接收保持可用状态并接受其他请求连接,这样做是为了使新的套接字描述符和原始套接字具有相同的地址族domain
和套接字类型type
。
如果服务器调用accept
并且当前没有连接请求,服务器会阻塞直到一个请求到来。如果不关心客户端标识,可以将参数addr
和len
设为NULL
。
注:关键字restrict
是C99新引入的,所有修改该指针所指向内容的操作全部都是基于该指针的,即不存在其它进行修改操作的途径;从而帮助编译器进行更好的代码优化,生成更有效率的汇编代码[4]。
3.5 读取数据
ssize_t read(int fd, void *buf, size_t nbytes); //成功返回读到的字节数,已到文件末尾返回0,出错返回-1
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回字节计数的消息长度,无可用消息或对方已经按序结束返回0,出错返回-1
ssize_t recvfrom(int sockfd,void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socketlen_t *restruct addrlen); //成功返回字节计数的消息长度,无可用消息或对方已经按序结束返回0,出错返回-1
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);//成功返回字节计数的消息长度,无可用消息或对方已经按序结束返回0,出错返回-1
可以使用read
通过套接字通信,但read
只能交换数据,若想指定选项、从多个客户端接收数据包,则需选择套接字函数recv
(指定标志控制接收数据的方式)、recvfrom
(得到数据发送者的源地址)、resvmsg
(将接收到数据送入多个缓冲区或接收辅助数据)。
3.6 写入数据
ssize_t write(int fd, void *buf, size_t count); //成功返回写入字节数,出错返回-1
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回发送的字节数,出错返回-1
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen); //成功返回发送的字节数,出错返回-1
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); //成功返回发送的字节数,出错返回-1
注:send
与sendto
的flags
含义相同,sendmsg
的flags
与前两者不同
可以使用write
通过套接字通信,但write
只能交换数据,若想指定选项、发送带外数据,则需选择套接字函数send
(指定标志改变处理传输数据的方式)、sendto
(允许无连接的套接字上指定一个目标地址)、sendmsg
(指定多重缓冲传输数据)。
3.7 终止连接
int close(int fd); //成功返回0,出错返回-1
int shutdown(int sockfd, int how);//成功返回0,出错返回-1
关闭套接字close
只有在最后一个活动引用被关闭后才释放网络端点,而shutdown
提供更精细的控制,套接字通信是双向的,可以用shutdown
禁止套接字上的输入/输出,即how
为SHUT_RD
、SHUT_WR
、SHUT_RDWR
。除此之外,shutdown
允许使一个套接字处于不活动状态(不管引用它的文件描述符数目多少),便于复制一个套接字(如dup
)。
4. 客户端操作
除了建立连接connect函数,客户端涉及到的操作在第三节都给出了。
使用函数connect建立连接,
//成功返回0,出错返回-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
addr
是想与之通信的服务器地址,如果sockfd
没有绑定到一个地址,connect
会给调用者绑定一个默认的地址。成功连接需要以下条件:要连接的机器开启且正在运行,服务器绑定到一个想与之连接的地址,服务器的等待连接队列有足够的空间。