什么是Socket
Socket是用于计算机之间进行网络通信的端口的抽象。提供了应用进程利用底层网络协议交换数据的机制。
通过TCP/IP协议栈进行网络通信的过程中,每个设备都需要唯一的IP地址信息进行标识,Socket允许应用程序通过IP地址进行通信,而不需要关心底层TCP/IP协议的具体实现,在这个过程中,可以将Socket理解为对TCP/IP的进一步封装。
同时,Socket也可以在本地进程之间通信,Socket允许应用绑定本地Socket文件,通过Socket文件进行进程之间数据的交换。
Socket通信过程
- 服务端:
- 创建Socket
- 绑定Socket: Socket支持绑定本地Socket或者IP端口
- 服务端开始监听客户端连接请求,当监听到连接请求后,调用accept()接收连接请求,建立服务端客户端连接
- 服务端客户端开始通信
- send(): 通过send接口发送数据
- receive(): 通过receive接口接收数据
- 服务端客户端处理通信异常信息
- 服务端主动关闭连接,或监听到客户端异常断开后,关闭连接
- 客户端:
- 创建Socket
- 请求连接服务端Socket
- 服务端客户端开始通信
- send(): 通过send接口发送数据
- receive(): 通过receive接口接收数据
- 服务端客户端处理通信异常信息
- 客户端主动关闭连接,或监听到服务端异常断开后,关闭连接
C++ Socket通信API
C++在进行Socket通信时,通常有以下常用接口:
int socket(int domain, int type, int protocol);
以下参数信息为常用参数信息,更多内容参考
man socket
- int socket(int domain, int type, int protocol):用于创建Socket
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- domain:网络连接协议簇的标识
- AF_INET:IPv4协议簇
- AF_INET6:IPv6协议簇
- AF_UNIX/AF_LOCAL:本地Socket
- type:Socket通信的语义类型
- SOCK_STREAM:提供有序、可靠、双向、基于连接的字节流。可以支持带外数据传输机制(TCP通信常用)
- SOCK_DGRAM:支持数据报(固定最大长度的无连接、不可靠的消息)(UDP通信常用)。
- protocol:与Socket通信语义相匹配的协议类型,注意,protocol的选择需要和语义类型匹配,不可以随意组合
- 0:根据type自动匹配protocol
- IPPROTO_TCP:TCP协议
- IPPROTO_UDP:UDP协议
- domain:网络连接协议簇的标识
- 返回值:
- 成功:返回Socket文件描述符ID
- 失败:返回-1,并设置errno
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen):socket()创建socket指定其协议类型后,我们还需要把socket绑定到具体的地址上才可以使用
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:上述通过
socket()
函数创建的socketID信息 - addr:socket绑定的协议地址,该参数需要与socket创建时指定的协议类型相关联
- addrlen:addr结构的字节大小
- sockfd:上述通过
- 返回值:
- 成功:返回0
- 失败:返回-1,并设置errno
struct sockaddr
|
|
在sockaddr
结构体中,数据信息混合在了一起,针对TCP/UDP这种包含IP和Port等多个数据的类型时,不是很好区分,所以一般情况下通过定义对应协议的addr信息,然后通过类型转换为soctaddr
结构体
struct sockaddr_un
|
|
struct sockaddr_in / struct sockaddr_in6
|
|
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen):客户端向服务端发送连接请求,等待连接
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:上述通过
socket()
函数创建的socketID信息,该sockfd为客户端向服务端发起请求的socket - addr:socket绑定的协议地址,该参数需要与socket创建时指定的协议类型相关联
- addrlen:addr结构的字节大小
- sockfd:上述通过
- 返回值:
- 成功:返回0
- 失败:返回-1,并设置errno
int listen(int sockfd, int backlog);
- int listen(int sockfd, int backlog):设置socket队列,监听socket请求,以处理客户端的连接、断开操作
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:上述通过
socket()
函数创建的socketID信息 - backlog: socket监听可能的最大连接长度,若队列已满,新的客户端连接请求会收到
ECONNREFUSED
错误
- sockfd:上述通过
- 返回值:
- 成功:返回0
- 失败:返回-1,并设置errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen):监听sockfd队列的第一个连接请求,创建连接socket,并返回引用该socket的文件描述符
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:上述通过
socket()
函数创建的socketID信息,该sockfd为客户端向服务端发起请求的socket - addr:socket绑定的协议地址,该参数需要与socket创建时指定的协议类型相关联
- addrlen:addr结构的字节大小
- sockfd:上述通过
- 返回值:
- 成功:返回socket文件描述符ID
- 失败:返回-1,并设置errno
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
socket 数据的常用发送接收还有
read()
,write()
和其他方法。
read()
和write()
方法在遇到\0
类型数据时会终止,所以通过该方法发送数据是受限的,这里不做介绍
- ssize_t send(int sockfd, const void *buf, size_t len, int flags):发送指定长度的数据
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:客户端和服务端已建立的socketID文件描述符信息
- buf:需要发送的数据内容
- len: buffer数据长度
- flags: 消息发送或运算的特性,默认可为0
- 返回值:
- 成功:成功发送数据的字节数
- 失败:返回-1,并设置errno
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags):接收指定长度的数据
- 头文件:
- #include <sys/types.h>
- #include <sys/socket.h>
- 参数:
- sockfd:客户端和服务端已建立的socketID文件描述符信息
- buf:接收数据的地址
- len: buffer数据长度
- flags: 消息接收或运算的特性,默认可为0
- 返回值:
- 成功:成功发送数据的字节数
- 失败:返回-1,并设置errno
int close(int fd);
- int close(int fd):关闭socket连接
- 头文件:
- #include <unistd.h>
- 参数:
- fd:已连接的文件描述符ID
- 返回值:
- 成功:0
- 失败:返回-1,并设置errno
C++ Socket通信
若要使用IPv4-TCP通信,请注释UNIX,取消注释IPv4
本示例不对IPv6通信进行说明
服务端
|
|
客户端
|
|
C++ Socket通信改进
以上代码可以实现简单的Socket通信流程,但是存在问题:
- 服务端在第一次启动时,会创建本地Socket套接字文件,在以IP协议进行通信的Socket网络通信中,Socket关闭会释放端口,但是以UNIX本地Socket进行通信的Socket网络通信在关闭后,不会主动删除创建的Socket文件。在再次通信的过程中,会绑定失败
- 解决方案:在程序第一次绑定时,判断文件是否存在,存在则删除
- 在Socket通信过程中,
send
,recv
,accept
,connect
等函数为阻塞函数,所以一旦调用此类方法,程序阻塞,就不能接收其他连接和处理了- 解决方案1:采用非阻塞模型进行Socket通信
- 解决方案2:采用线程的方式,当新的socket连接建立后,将socket通信的过程放在线程中执行,线程结束释放该socket通信,这样可以通过
accept
方法以阻塞的方式一直等待新的客户端连接 - 这里采用方案2进行改进
- Socket通信过程中,采用
send
,recv
方法虽然解决了write
,read
方法不能发送\0
的问题,但是存在的问题是,通过send
,recv
传输数据需要已知数据大小,在数据大小确定的情况下,该方案很符合场景需要,但是在数据大小不确定的情况下。- 解决方案1: 确定"大"数据长度,数据长度不足后面补标志位、
- 解决方案2:在传输数据前,先发送数据大小,然后初始化buffer接收数据
- 这里采用方案2进行改进
在以下改进中,统一采用UNIX模型进行通信,IP模型请自行替换
UNIX socket文件已存在
|
|
子线程通信
头文件:
|
|
|
|
Socket通信流程改进
服务端
核心代码
|
|
头文件
|
|
全代码
|
|
客户端
|
|