文章目录
Socket 简介
Socket,即套接字,可用于网络中两个不同主机上的进程间通信
最早由 BSD 实现,后来Unix、Linux、Win等均有实现及拓展
先看一下 Socket 编程在计算机网络中所处的位置
实际上,SOCK_RAW 类型的 socket 是直接面向网络层的
Socket API 主要面向 TCP/IP协议栈,为 Client/Sever 通信模型提供支持
对外使用 IP地址与端口号(port)标识通信端点,即用 IP + port 作为通信端点地址
进程使用套接字描述符(socket descriptor,类似文件描述符) 对socket 进行管理
与文件管理类似,套接字描述符表的每个表项都指向一个保存套接字信息的数据结构
套接字描述符即是表中的下标
Socket 编程的基本操作
各平台的 Socket 实现核心部分基本一致,下面简单介绍
socket
创建套接字
int socket(int domain, int type, int protocol);
参数
- 指明协议族,如 PF_INET 表示 TCP/IP
- 指明创建的 socket 类型,SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW 等
- 指明具体协议,可配合
getprotobyname
使用
返回值:创建成功时返回 socket descriptor,否则返回 -1
int sd = socket(PF_INET, SOCK_DGRAM, getprotobyname("tcp")->p_proto);
其中,SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW 类型分别对应于
TCP、UDP 和网络层协议(如 IP、ICMP、IGMP
TCP:可靠、有连接、字节流传输、点对点
(一个 TCP socket 只能与一个 TCP socket 通信、也只能建立一次连接
UDP:不可靠、无连接、数据报传输
(UDP socket 可多次指定目标地址
close / closesocket
关闭指定描述符对应的 socket,Win 下为 closesocket
int close(int sd);
参数:要关闭的 socket 的描述符
返回值:成功则返回 0,失败返回 -1
注意:多个进程共享的 socket 会有引用计数,一个进程中 close() 引用计数减 1
减至 0 时 socket 关闭;而同一进程的多个线程间共享的 socket 不进行引用计数
bind
为套接字绑定本地端点地址(IP 与 port
int bind(int sd, const struct sockaddr *localaddr, int addrlen);
参数
- 套接字描述符
- 端点地址,根据 socket 类型可能有多种结构体,因此指定 addrlen,表明该结构体长度
- 参数 2 的实际类型长度
返回值:成功则返回 0,否则返回 -1
注意:服务端 socket 需要 bind(),客户端 socket 一般无需使用 bind() 而由操作系统完成
// Declare variables
SOCKET ListenSocket;
struct sockaddr_in saServer;
hostent* localHost;
char* localIP;
// Create a listening socket
ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Get the local host information
localHost = gethostbyname("");
localIP = inet_ntoa (*(struct in_addr *)*localHost->h_addr_list);
// Set up the sockaddr structure
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = inet_addr(localIP);
saServer.sin_port = htons(5150);
// Bind the listening socket using the
// information in the sockaddr structure
bind( ListenSocket,(SOCKADDR*) &saServer, sizeof(saServer) );
//以上来自于 microsoft docs
若服务器有两张网卡,分别连接了不同的网络,作为服务端如果给 socket
设定其中一个网卡的 IP,那么就无法接受发送到另一个 IP 的请求
面对这样的情况,可以将地址设为通配符 INADDR_ANY,即
saServer.sin_addr.s_addr = INADDR_ANY;
listen
将服务端套接字置于监听状态(仅服务端、流式套接字(TCP套接字)使用
int listen(int sd, int queuesize);
参数
- 调用 listen() 的 socket descriptor
- 等待队列的最大大小
返回值:成功则返回 0,否则返回 -1
connect
使客户端 socket 与特定计算机特定端口的 socket 进行连接
int connect(int sd, const sockaddr *addr, int addrlen);
参数:
- 发起连接的客户端 socket
- 要连接的服务端端点地址
- 参数 2 的实际类型长度
返回值:成功则返回 0,否则返回 -1
connect() 用于 TCP 客户端 socket 时,将向服务端点 addr 发送建立 TCP 连接请求,此时 connect() 是阻塞的
用于 UDP 客户端 socket 时,仅仅为客户端 socket 指定了服务端点地址 addr
也就是说,TCP socket 调用 connect() 成功后即建立连接可以通信,UDP socket 只是指定了通信端点
注意:TCP socket 只能调用一次 connect(),UDP socket 可以多次调用 connect()
accept
从监听状态的服务端 socket 接收客户端连接请求,然后创建一个新的 socket 连接客户端 socket
(仅服务端、流式套接字(TCP套接字)使用
int accept(int sd, struct sockaddr *addr, int *addrlen);
参数:
- 监听状态(listen)的服务端 socket
- 新创建套接字的端点地址结构体
- 参数 2 的实际长度
返回值:返回响应客户端 socket 请求所创建的 socket 描述符,以用于通信
当请求队列为空时,调用 accept() 的线程阻塞
send、sendto
向一个 socket 所指定的端点地址发送数据
//win
int send(SOCKET sd, const char *buf, int len, int flags);
//Linux
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
- 发送数据所使用的 socket
- 数据起始地址
- 数据长度
- 一组标志,用于指定调用的方式,通常为 0
返回值:函数返回实际发送的字节数,可能小于 len,出错则返回 -1
注意,调用 send() 的套接字必须已有通信端点地址,如建立连接的 TCP socket、调用过 connect() 的 UDP socket
若 socket 未指定过通信端点,可使用 sendto() 在通信时指定
//win
int sendto(SOCKET sd, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
//Linux
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
同理,使用 sendto() 的 socket 只能是 UDP socket
对于已经使用 connect() 绑定过端点地址的 socket,sendto() 仅改变本次发送的目标地址
send(sd, buf, len, flag)
相当于 sendto(sd, buf, len, flag, NULL, 0)
此外,Socket API 还提供了 sendmsg() / WSASendMsg() 等写入函数
recv、recvfrom
recv() 用于 TCP socket 从连接的另一端接收数据,或用于调用过 connect() 的 UDP 客户端 socket 接收数据
recvfrom() 用于 UDP 服务器 socket 或未调用过 connect() 的 UDP socket 接收数据
//win
int recv(char *buf, int len, int flags);
int recvfrom(SOCKET sd, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);
//Linux
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
返回值:返回接收的字节数,或 -1 表示出错
此外,Socket API 还提供了 recvmsg() 等函数接收数据
setsockopt、getsockopt
设置 / 获取套接字选项对应的取值
int setsockopt(SOCKET s, int level, int optname, const char *optval, int optlen);
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
参数
- 标识套接字的描述符
- 定义选项的级别(如 SOL_SOCKET)
- 要为其设置值的套接字选项(如 SO_BROADCAST)。optname参数必须是在指定级别内定义的套接字选项,否则是未定义行为
- 指向存放选项值的缓冲区的指针
- optval 参数指向的缓冲区的大小
返回值:成功时返回 0,失败返回 -1
注意,以下以 WSA 开头的 API 均为 Win 下特有的或需要的 API
Win 下需包含 WinSock2.h 头文件
WSAStartup
Win 下使用 Socket 前必须调用该函数,用于引入相应的 DLL
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData);
参数
- 指明所使用的 WinSock 版本,高位字节表示副版本号,低位字节表示主版本号
- 为具体版本信息结构体的指针,指定的 WinSock 具体版本信息将保存到传入的结构体中
返回值:成功则返回 0,失败返回非零值
WSADATA wsaData;
wVersionRequested = MAKEWORD(2,1);//version 2.1
err = WSAStartup(wVersionRequested, &wsaData);
WSACleanup
不再使用 Socket 后,调用该函数解除与 DLL 的关联,释放相应资源
int WSACleanup();
返回值:成功则返回 0
字节序
此外,网络中的各个机器可能采用不同的字节序,那么通信时应当统一一种机器无关的字节序
TCP/IP 定义了标准的用于协议头中的二进制数字节序
使用如下函数在本地字节序与网络字节序中进行转换
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
如 htons,即 host to network short
socket 通信的一般流程
close 一个 TCP socket 时,也会给对方发送连接关闭的信息,图中未表示出
2020/5/1