Socket编程流程
服务端:socket—>bind—>listen—>accept—>send/recv—>closesocket
客户端:socket—>bind(可选)—>connect—>send/recv---->closesocket
创建Socket
socket是通信端点的抽象,使用socket描述符来标识,类似文件描述符,通过调用socket函数创建。
#include <sys/socket.h>
int socket (int family, int type, int protocol);
返回值:成功,返回socket描述符,失败,返回-1
family用于指明协议族,取值范围如下所示:
取值 | 含义 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_UNIX | UNIX域协议,可用于进程间通讯 |
AF_UPSPEC | 未指定域 |
AF_ROUTE | 路由套接字,内核路由表接口 |
AF_KEY | 密钥套接字,内核密钥表接口 |
type用于确定套接字的类型,取值范围如下所示:
取值 | 含义 |
---|---|
SOCK_DGRAM | 数据报套接字,提供固定长度、无连接、不可靠的报文传递,可用于UDP |
SOCK_RAW | 原始套接字,可用于IP协议编程 |
SOCK_SEQPACKET | 有序分组套接字,固定长度、有序的、可靠的、面向连接的报文传递,可用于SCTP |
SOCK_STREAM | 字节流套接字,有序的、可靠的、双向的、面向连接的字节流,可用于TCP |
protocol用于确定协议类型,取值范围如下所示:
取值 | 含义 |
---|---|
0 | 选择默认协议,通常使用 |
IPPROTO_IP | ipv4 |
IPPROTO_IPV6 | ipv6 |
IPPROTO_ICMP | ICMP |
IPPROTO_RAW | 原始IP数据包 |
IPPROTO_TCP | TCP |
IPPROTO_UDP | UDP |
SOCK_STREAM和SOCK_SEQPACKET区别:前者基于流,无法区分报文的界限,可能需要多次函数调用才能获取一个完整数据报文,后者单次调用就能获得完整报文。
SOCK_RAW作用:提供数据报接口,用于直接访问IP层,应用程序需要自己构造协议头部。
关闭套接字
调用close函数关闭套接字,其本质是将套接字的引用计数减1,当引用为0,将套接字标记成关闭状态,不可调用read或write函数,发送队列上的等待的数据将发往对端,随后进入TCP四次挥手流程。
#include <unistd.h>
int close(int sockfd)
返回值:成功,返回0,出错,返回-1
使用close时,只有最后一个进程close,才会释放socket。shutdown可以立即禁用socket,并且可以指定禁用方式。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
返回值:成功,返回0,失败,返回-1
how表示以什么方式禁用,取值范围:
- SHUT_RD:关闭读端,无法读数据
- SHUT_WR:关闭写端,无法发送数据
- SHUT_RDWR:不能读也不能发送数据
套接字绑定地址
服务端套接字需要先调用bind函数绑定地址和端口后,才能调用listen函数,将套接字转换为监听套接字,进入监听状态。客户端调用connect之前可选择性调用bind,如果不调用,由系统决定使用哪个源IP和port。bind函数的定义如下所示:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
- 返回值:成功,返回0,失败,返回-1
- sockfd:本地有效socket描述符
- addr:通用socket地址指针
- len:socket地址长度
在给socket绑定sockaddr时,可选择绑定IP、端口中的0个或多个,产生的结果如下所示:
IP地址 | 端口 | 结果 |
---|---|---|
INADDR_ANY | 0 | 内核决定IP和端口 |
INADDR_ANY | 非0 | 内核决定IP,进程决定端口 |
特定IP地址 | 0 | 进程决定IP,内核决定端口 |
特定IP地址 | 非0 | 进程决定IP和端口 |
bind函数存在以下限制:地址中的端口必须大于1024,一个sockfd只能绑定一个地址
socket和地址绑定成功之后,可以调用getsockname
获取socket绑定的地址信息,如果和服务端建立了连接,可以通过getpeername
获取对端的地址信息。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t * restrict alenp);
- 返回值:成功,0,失败,-1
- sockfd:socket文件描述符
- addr:通用套接字地址
- alenp:通用套接字地址长度
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
- 返回值:成功,0,失败,-1
- sockfd:socket文件描述符
- addr:通用套接字地址
- alenp:通用套接字地址长度
服务端进入监听
仅有服务端调用,服务端调用listen
函数进入监听状态,此时客户端可以调用connect
函数与服务端建立TCP链接。socke
创建的套接字默认是主动套接字,即客户端套接字,用于调用connect
发起连接,listen
将未连接的套接字,转换成被动套接字,指示内核应接收指向该套接字的连接请求。
内核为被动套接字维护两个队列:未完成连接队列和已完成连接队列。未完成连接队列维护处于半连接状态的连接,服务端收到一个SYN包时,在未完成连接队列里创建一项,当该连接状态转为Establish时,移到已完成连接队列尾部。accept
函数从已完成连接队列头部返回一个连接。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- 返回值:成功,0,失败,-1
- sockfd:服务端socket
- backlog:最大的等待连接队列数目
- backlog不会超过SOMAXCONN,一旦队列满了,就会拒绝多余的请求
- 调用了listen之后,才能调用accept获得连接并建立连接
- listen不会阻塞,listen之后,客户端就可以通过connect建立三次握手,accept只是从三次握手成功的队列中出一个
- linten的套接字转换成监听套接字,内核才会接收连接该套接字的外部连接。accept返回套接字称为已连接套接字
客户端发起连接请求
客户端调用connect
函数,向服务端发起请求。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
- 返回值:成功,0,失败,-1
- sockfd:本地的socket描述符
- addr:对侧的地址,如果本侧socket未绑定地址,则绑定默认地址
- len:addr长度
重连机制:如果连接失败,有的系统支持使用同一个socket描述符重新调用connect,有的操作系统则要求重新创建socket,因此为了移植性,建议使用后者
阻塞connect:如果套接字描述符处于非阻塞模式,在连接不能马上建立时,connect返回-1,并将errno设置为EINPROGRESS,调用poll、select或epoll判断描述符何时可写,如果可写,则连接完成
UDP调用connect:UDP等无连接的传输,也可以调用connect,将报文的目的地址设置为connect指定的地址
获得连接
服务端调用accept,从已完成队列中,返回下一个Establish状态的连接。
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);- 返回值:成功,返回已连接套接字,失败,返回-1- sockfd:监听套接字- addr:客户端的地址存放的缓存,不关注的话可以设置为NULL- len:accept会更新len,指向实际客户端地址结构的长度,不关注的话可以设置为NULL
返回值是一个服务端的套接字描述符,和原始的sockfd有相同的地址族、协议和type,服务端使用该套接字与客户端通讯,而源sockfd继续保持监听状态用于接收新的连接,这也是一个服务端可以被多个客户端连接的原因。
如果sockfd处于阻塞模式,如果已完成队列为空,那么accept阻塞;如果sockfd处于非阻塞模式,返回-1,errno设置为EAGAIN或EWOULDBLOCK