C++跨平台socket编程
- 一、概述
- 1.1 TCP协议
- 1.1 TCP 的主要特性
- 1.2 TCP报文格式
- UDP报文格式
- IP协议
- 使用windows编辑工具直接编辑Linux上代码
- 二、系统socket库
- 1.windows上加载socket库
- 2.创建socket
- 2.1 windows下
- 2.2 linux下
- 3.网络字节序
- 4.bind端口
- 5.listen监听并设置最大连接数
- 6.accept读取用户连接信息
- 7.服务器通过recv接收客户端发送信息
- 服务器循环接收客户端数据
- 8.服务器send回应客户端数据
- 9.服务器开启多线程并发处理客户端连接
- 9.1 windows下
- 9.2 linux下
- 三、封装跨平台XTcp类
- 1.windows下测试
- 2.linux下测试
- 3.创建并测试XTcp的dll动态链接库
- 3.1 创建
- 3.2 测试
- 4.创建并测试XTcp的so动态链接库
- 4.1 创建
- 4.2 测试
- 四、TcpClient编写与tcp编程总结
- 1.connect与三次握手的过程
- 1.1 为什么需要三次握手?
- 2 connect客户端连接服务器
- 3.tcp编程总结
- 五、高并发服务器开发和测试
- 1.Windows中设置socket阻塞和非阻塞
- 2.Linux中设置socket阻塞和非阻塞
- 3.通过select实现connect的超时处理
- 为什么使用select实现connect的超时处理?
- 4.并发测试工具ab
- 5.基于epoll的高性能服务器
- 注意
- 6.epoll、select 和阻塞 accept 之间的关系
- 1.监听套接字队列
- 2.**accept 函数**
- 3.I/O 多路复用机制
- 六、http协议
- 请求
- 2.**accept 函数**
- 3.I/O 多路复用机制
- 六、http协议
- 请求
一、概述
1.1 TCP协议
TCP协议提供可靠、有序、且无差错的数据传输服务。
1.1 TCP 的主要特性
面向连接:
- 在传输数据之前,TCP 需要在通信双方之间建立一个连接,这个过程称为三次握手(Three-way Handshake)。
可靠传输:
- TCP 通过确认(ACK)、序列号、超时重传等机制保证数据的可靠传输。
有序传输:
- TCP 确保数据按序到达目标,即使数据包乱序到达,TCP 也会重新排序。
流量控制:
- TCP 使用滑动窗口机制来控制数据流量,避免发送方发送数据过快,超过接收方的处理能力。
拥塞控制:
- TCP 有内置的拥塞控制算法,通过调整发送速率避免网络拥塞。
1.2 TCP报文格式
UDP报文格式
IP协议
使用windows编辑工具直接编辑Linux上代码
ubuntu: apt-get install samba
centos7:sudo yum install samba samba-client samba-common -y
配置环境:
vim ../etc/samba/smb.conf
文件末尾加入共享目录:
重启一下服务:
在根目录下创建code目录:
设置权限:
添加Samba用户,设置密码:
检查服务是否都启动了(ubuntu跳过):
如果服务没有启动(centos):
sudo systemctl enable smb nmb
sudo systemctl start smb nmb
如果服务没有启动(ubuntu):
sudo service smbd restart
在windows系统中连接共享目录:
如果再次连接samba,并尝试进入共享文件夹,出现报错你没有权限访问,可能是防火墙问题,输入命令:
sudo setenforce 0
sudo iptables -F
二、系统socket库
套接字(Socket)是网络编程中用于在计算机之间进行通信的端点。它提供了一种进程间通信的机制,可以在同一台计算机上或不同计算机之间进行数据交换。套接字的概念和实现是网络通信的重要基础。
套接字是一个主机本地应用程序创建的,被操作系统所控制的接口(“门”)。
应用程序通过这个接口,使用传输层提供的服务,跨网络发送(接收)消息到(从)其他应用进程。
C/S模式的通信接口——套接字接口。
1.windows上加载socket库
在Windows中,Winsock库不是自动加载的。在使用网络功能之前,程序必须显式地初始化Winsock库,以便系统可以分配资源并为网络通信做好准备。初始化之后,程序就可以创建套接字并使用网络功能了。
每次编写涉及套接字编程的应用程序时,都需要在程序的开头部分添加这两行代码,并在程序结束时调用 WSACleanup()
来清理资源。
2.创建socket
2.1 windows下
socket(AF_INET, SOCK_STREAM, 0)
:创建一个套接字。
AF_INET
指定使用IPv4地址族,SOCK_STREAM
指定使用流式套接字(TCP),0
表示使用默认的协议(对于 SOCK_STREAM
即TCP协议)。socket
函数成功时返回一个非负整数表示创建的套接字,失败时返回 -1
。
2.2 linux下
因为Linux下各种库和windows不一样,个别函数也不一样,因此使用条件编译的方法。
我们一次性创建两千个socket。
在windows下,可以正常创建。
在Linux下,因为系统默认一个进程最多创建1024个socket,因此1024之后的创建不了。
我们可以通过ulimit -n修改最大可创建socket个数。
此时再运行就可以了。
3.网络字节序
网络字节序(Network Byte Order)是指在网络传输中,数据的字节排列顺序。它采用的是大端字节序(Big-Endian),即最高有效字节(Most Significant Byte,MSB)在前,最低有效字节(Least Significant Byte,LSB)在后。
#include <arpa/inet.h>
//将主机字节序转换为网络字节序
unit32_t htonl (unit32_t hostlong);
unit16_t htons (unit16_t hostshort);
//将网络字节序转换为主机字节序
unit32_t ntohl (unit32_t netlong);
unit16_t ntohs (unit16_t netshort);
4.bind端口
获取端口号
配置地址结构
绑定端口
在Linux中编译发现报错,说明有头文件不一致。
5.listen监听并设置最大连接数
int listen(int sockfd, int backlog);
参数说明
sockfd
:这是一个已经通过socket
和bind
函数创建并绑定到特定地址和端口的套接字描述符。backlog
:指定等待连接队列的最大长度,即在accept
函数被调用之前,可以有多少个连接请求处于等待状态。
当一个服务器套接字处于监听状态时,它会创建一个队列来存储那些尚未被服务器接受(accept
)的传入连接请求。当完全连接队列已满且有新的连接到达时,新连接可能会被拒绝(客户端将收到一个错误)。这个队列实际上包含两个部分:
-
半连接队列:存放已经完成三次握手中的第一步(SYN)但尚未完成整个握手过程的连接请求。
-
完全连接队列:存放已经完成三次握手的连接请求,等待应用程序调用
accept
。
在实际应用中,backlog
的值会影响服务器在高并发环境下的表现。设置过低的 backlog
值可能导致拒绝合法的连接请求,而设置过高的值可能会消耗更多的系统资源。
6.accept读取用户连接信息
accept
函数是套接字编程中用于从监听队列中提取第一个连接请求的系统调用。它会创建一个新的套接字用于与客户端进行通信。accept
函数通常与服务器端的监听套接字配合使用,在客户端发起连接请求并经过 listen
函数处理后,由 accept
函数接受该请求。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
sockfd
:已绑定到本地地址并处于监听状态的套接字描述符。addr
:指向sockaddr
结构体的指针,用于存储客户端的地址信息。可以是NULL
。addrlen
:指向一个socklen_t
变量的指针,用于存储addr
结构体的大小。调用前应设置为addr
结构体的大小,调用后包含实际的地址长度。可以是NULL
。
返回值
- 成功:
accept
函数成功时返回一个新的套接字描述符,这个新的套接字用于与客户端进行通信。- 返回值是一个非负整数,表示新的连接套接字。
- 失败:
accept
函数失败时返回-1
,并设置errno
以指示错误类型。
7.服务器通过recv接收客户端发送信息
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明
sockfd
:一个已经连接的套接字描述符。buf
:指向用于存放接收到的数据的缓冲区。len
:缓冲区的长度,即可以接收的数据的最大字节数。flags
:接收操作的标志,可以是0或以下值的组合:MSG_OOB
:接收带外数据。MSG_PEEK
:窥视操作,将数据复制到缓冲区但不从输入队列中删除。MSG_WAITALL
:等待完整的数据,直到缓冲区满或发生错误。MSG_DONTWAIT
:非阻塞操作,如果没有数据可读,立即返回。
服务器循环接收客户端数据
不过现在有一个新的问题,如果有第二个、第三个…多个客户端连接,只有当第一个客户端退出,服务器才能收到其他客户端的消息,这是因为我们是在单线程中处理消息。
8.服务器send回应客户端数据
9.服务器开启多线程并发处理客户端连接
9.1 windows下
9.2 linux下
三、封装跨平台XTcp类
为什么需要XTcp类?
与系统相关的头文件、socket的初始化、绑定等函数容易出错,同时写起来很麻烦,因此包装成一个类,直接调用类的方法。
- 构造函数
- 创建和关闭socket
- 关于为什么提供CloseSocket而不是直接析构中调用closesocket?
- 析构中调用WSACleanup()同理,目前先不做析构。
- 关于为什么提供CloseSocket而不是直接析构中调用closesocket?
- 绑定与监听
- 接收客户端连接
- 发送、接收数据
- main 和 Tcpserver
1.windows下测试
2.linux下测试
3.创建并测试XTcp的dll动态链接库
3.1 创建
注意,在Windows系统上,套接字编程需要链接 Ws2_32.lib
库。确保在项目设置中包含这个库。
如果程序需要执行,则要配置工作目录为dll所在目录。
3.2 测试
4.创建并测试XTcp的so动态链接库
4.1 创建
运行makefile会发现有报错,windows的导出宏在linux没有,还有头文件只能包含一次也没有。
4.2 测试
再把测试程序也编译一下。
根据提示修改makefile文件
在Linux中,运行程序时确实需要确保动态库的路径在系统的库搜索路径中,否则程序将无法找到并加载所需的动态库。
四、TcpClient编写与tcp编程总结
1.connect与三次握手的过程
1.1 为什么需要三次握手?
确保双方都有能力发送和接收数据:通过三次握手,客户端和服务器可以确认双方都能够发送和接收数据包。
同步初始序列号:三次握手的过程还可以确保双方同步初始序列号,以防止因网络传输延迟或重传而导致的数据包混乱。
防止旧的重复连接初始化:三次握手可以防止旧的、重复的连接请求在网络中滞留并意外创建连接。
如果只进行两次,客户端收到服务器发送的ack后,知道服务器能收到客户端消息,所以客户端能保证服务器收到正确数据。但是如果客户端不回复服务器这一次的syn,那么服务器不知道客户端能否正确收到服务器的消息。
2 connect客户端连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- sockfd:由
socket
函数返回的套接字文件描述符。它标识一个套接字。 - addr:指向
struct sockaddr
类型的指针,包含了要连接的服务器的IP地址和端口号。 - addrlen:结构体
sockaddr
的长度。
返回值:
- 成功:返回0。
- 失败:返回-1,并设置
errno
以指示错误原因。
3.tcp编程总结
五、高并发服务器开发和测试
1.Windows中设置socket阻塞和非阻塞
阻塞模式:
- 套接字在创建时默认是阻塞模式。
- 调用套接字函数(如
accept
、connect
、recv
、send
等)时,如果操作无法立即完成,函数会阻塞(即挂起执行)直到操作完成或发生错误。非阻塞模式:
- 调用套接字函数时,如果操作无法立即完成,函数立即返回一个错误(如
EWOULDBLOCK
),而不是阻塞等待。- 程序通常需要轮询套接字状态或使用事件驱动机制(如
select
、poll
或epoll
)时使用非阻塞模式。
我们的目的是:在建立连接的时候是非阻塞模式,在接收数据的时候是阻塞模式,因为接收数据我们是多线程方式,所以设置为阻塞不影响别的线程运行。
ioctlsocket
是 Windows 操作系统中用于控制套接字行为的函数。
int ioctlsocket(
SOCKET s,
long cmd,
u_long *argp
);
参数说明:
- s: 套接字的描述符。
- cmd: 控制命令,指定要执行的操作。
- argp: 命令参数,根据
cmd
指定不同的含义。
FIONBIO: 设置或清除非阻塞模式。
- 如果
*argp
为非零值,则套接字设置为非阻塞模式。 - 如果
*argp
为零,则套接字设置为阻塞模式。
返回值:
- 如果函数成功,返回 0。
2.Linux中设置socket阻塞和非阻塞
3.通过select实现connect的超时处理
为什么使用select实现connect的超时处理?
- 阻塞
connect
无法设置超时:connect
是一个阻塞操作,当服务器不可达时,可能会阻塞很长时间。这会导致程序响应变慢,尤其是在高并发环境下。
- 非阻塞
connect
:- 通过将套接字设置为非阻塞模式,
connect
会立即返回并设置errno
为EINPROGRESS
,表示连接正在进行中。此时,程序不会被阻塞,可以继续处理其他任务。
- 通过将套接字设置为非阻塞模式,
在非阻塞模式下使用 select
实现 connect
的超时处理是一个常见的做法,因为直接使用阻塞的 connect
函数无法设置超时。
select是一个用于多路复用的系统调用,用来监视多个文件描述符,等待其中的一个或多个文件描述符变为"就绪"状态,也就是可以进行I/O操作(如读或写)而不会阻塞。
在 select 函数中,有三个文件描述符集合用于监听不同类型的事件:
- 读集合(readfds):用于监听可读事件。读事件是指文件描述符上有数据可以读取,或者连接已经关闭,或有一个新的连接请求(对于监听套接字)。当某个文件描述符触发读事件时,select会在
readfds
集合中设置该文件描述符。 - 写集合(writefds):用于监听可写事件。写事件是指文件描述符可以执行写操作而不会阻塞。通常在以下情况下触发写事件:当套接字的发送缓冲区有空间时,select会在
writefds
集合中设置该文件描述符。 - 异常集合(exceptfds):用于监听异常事件。
文件描述符集合:
在 select
中,文件描述符集合使用 fd_set
结构。可以通过以下宏来操作文件描述符集合:
- FD_ZERO(fd_set *set):清空集合。
- FD_SET(int fd, fd_set *set):将文件描述符加入集合。
- FD_CLR(int fd, fd_set *set):将文件描述符从集合中删除。
- FD_ISSET(int fd, fd_set *set):检查文件描述符是否在集合中。
我们可以将需要监听的套接字放入套接字文件描述符集合,由该集合负责帮我们监听该文件描述符表中这些套接字文件描述符对应的套接字的缓冲区中是否有数据需要处理。
这个监听集合的大小为1024(默认最大值),但需要注意的是,虽然这个集合的大小为1024,但实际能帮我们监听的客户端套接字只有1020个,因为前1-3个分别用于监听标准输入、标准输出和标准出错,第四个用于存放服务器套接字。通过这个监听集合,我们就可以实现对多个socket的同时监听。
在非阻塞模式下调用 connect
函数时,它不会阻塞等待连接完成,而是立即返回。如果连接不能立即完成,errno
会被设置为 EINPROGRESS
,表示连接正在进行中。
当套接字连接成功或失败时,套接字会变为可写,因此应该监听写事件。
代码如下:
4.并发测试工具ab
5.基于epoll的高性能服务器
epoll
是 Linux 特有的 I/O 多路复用机制,适用于处理大量并发连接。它比传统的select
和poll
更高效,能够处理更高的并发数。
epoll
支持两种触发模式:边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。
水平触发(LT):
这是
epoll
的默认模式,类似于select
和poll
的工作方式。当文件描述符处于就绪状态时,epoll_wait
会返回该文件描述符,每次调用epoll_wait
都会返回,直到事件被处理。
优点:
简单直接,每次都有数据时都会通知。
缺点:
当不处理事件时,
epoll_wait
会不断返回就绪状态,可能导致重复处理同一事件。边缘触发(ET):
在边缘触发模式下,当文件描述符状态发生变化(如从不可读变为可读)时,
epoll_wait
会返回该文件描述符。事件只会在状态变化时触发,处理后不会再次触发,直到状态再次变化。
优点:
高效,减少了不必要的系统调用。
适用于处理高并发连接,避免重复处理同一事件。
缺点:
复杂,需要确保每次事件处理时读取或写入尽可能多的数据,避免丢失事件。
水平触发(level-trggered)
- 只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知。
- 当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知。
边缘触发(edge-triggered)
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知。
当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。
水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次。
epoll的主要组件:
epoll_create
:创建一个 epoll 实例。epoll_ctl
:控制 epoll 实例,注册、修改或删除感兴趣的文件描述符事件。epoll_wait
:等待事件的发生,并返回已经准备好的文件描述符。
注意
使用apache测试工具进行测试,发现进度卡在900条。
主要问题在于accept部分,如果同时有多个连接到来,并且都是请求与服务器进行的连接,我们只accept一次,其他的连接就丢失了,因为我们是边缘触发,只通知一次。
但是accept是阻塞函数,如果调用的时候没有新的连接到来了,就会一直阻塞着,因此我们需要首先设置套接字为非阻塞。
处理速度是5000多次每秒,是之前的八倍。
在if(events[i].data.fd == tcp.sock)判断条件下,可能会有多个客户端同时请求服务器,但是最开始只accept一次,会导致一些请求被丢失或者延迟处理。关于为什么设置tcp.sock为非阻塞,因为没法直接判断监听套接字队列是否为空,因此通过不停调用accept来间接判断,如果tcp.sock是阻塞,那么当队列为空,就会阻塞整个流程,导致后续只有新连接到来才会停止阻塞,反而tcp.sock为非阻塞,可以根据accept的返回值判断是否队列为空。
6.epoll、select 和阻塞 accept 之间的关系
1.监听套接字队列
当服务器调用 listen
函数将套接字设置为监听模式时,内核会为该套接字分配一个连接队列。
这个队列包含所有已完成三次握手但尚未被服务器 accept 的客户端连接。
队列与 I/O 多路复用机制(如 epoll、select 或阻塞的 accept)是独立的。
2.accept 函数
accept
函数用于从监听队列中提取一个已完成的连接,并为该连接创建一个新的套接字。
如果队列为空,阻塞模式下的 accept
函数会阻塞,直到有新的连接可用。非阻塞模式下的 accept
会立即返回,并设置 errno
为 EAGAIN
或 EWOULDBLOCK
。
3.I/O 多路复用机制
I/O 多路复用机制(如 select
、poll
和 epoll
)用于监视多个文件描述符,查看它们是否准备好进行 I/O 操作(如读或写)。
六、http协议
HTTP/1.0(短连接):
- 每个请求/响应对都要创建一个新的TCP连接,服务器在发送完响应后立即关闭连接。
HTTP/1.1(持续连接):
- 支持持久连接,即默认情况下,TCP连接会保持打开,允许在同一个连接上发送多个请求和响应。
- 支持分块传输编码,可以在响应主体的长度未知时逐块传输。
HTTP/2(二进制协议):
- HTTP/2 使用二进制格式传输数据,而不是HTTP/1.1 的文本格式。
- 在一个TCP连接上可以发送多个请求和响应,彼此互不干扰。
- 使用HPACK压缩算法减少头部大小,降低带宽消耗。
请求
请求方法 请求地址 协议版本 回车
(如 epoll、select 或阻塞的 accept)是独立的。**
2.accept 函数
accept
函数用于从监听队列中提取一个已完成的连接,并为该连接创建一个新的套接字。
如果队列为空,阻塞模式下的 accept
函数会阻塞,直到有新的连接可用。非阻塞模式下的 accept
会立即返回,并设置 errno
为 EAGAIN
或 EWOULDBLOCK
。
3.I/O 多路复用机制
I/O 多路复用机制(如 select
、poll
和 epoll
)用于监视多个文件描述符,查看它们是否准备好进行 I/O 操作(如读或写)。
六、http协议
HTTP/1.0(短连接):
- 每个请求/响应对都要创建一个新的TCP连接,服务器在发送完响应后立即关闭连接。
HTTP/1.1(持续连接):
- 支持持久连接,即默认情况下,TCP连接会保持打开,允许在同一个连接上发送多个请求和响应。
- 支持分块传输编码,可以在响应主体的长度未知时逐块传输。
HTTP/2(二进制协议):
- HTTP/2 使用二进制格式传输数据,而不是HTTP/1.1 的文本格式。
- 在一个TCP连接上可以发送多个请求和响应,彼此互不干扰。
- 使用HPACK压缩算法减少头部大小,降低带宽消耗。
请求
请求方法 请求地址 协议版本 回车
[外链图片转存中…(img-lHGhYC2W-1718795314397)]
[外链图片转存中…(img-zsc4OdXR-1718795314398)]