c++ 套接字网络编程
[toc]
socket
Created 2021.03.06 by William Yu; Last modified: 2021.03.06-V1.0.2
Contact: windmillyucong@163.com
Copyleft! 2021 William Yu. Some rights reserved.
c++ 网络编程,又称为套接字编程
References
- https://www.gta.ufrj.br/ensino/eel878/sockets/index.html
- https://www.cnblogs.com/DOMLX/p/9663167.html
- 《c++网络编程》
- https://zhuanlan.zhihu.com/p/119085959
1. 网络通信原理
- AB通信的过程为:
- A的应用层 -> A的传输层(TCP/UDP)-> A的网络层(IPV4,IPV6)-> A的底层硬件(物理信号)->
- B的底层硬件 -> B的网络层 -> B 的传输层 -> B的应用层
- socket工作与应用层和传输层之间
- TCP和UDP的区别:
- 连接和应用层数据的处理和发送方式
2. TCP
三个步骤
- 建立链接
- 收发数据
- 断开链接
建立链接:TCP三次握手
-
客户端为closed状态,服务端为listen状态
-
客户端发起连接,(socket中的connent()函数 ),客户端向服务器发送SYN包
-
SYN包非常小,不含实际数据
-
客户端状态由closed切换为syn_sent
-
服务端接受syn
- 服务端由listen转为syn_rcvd
- 返回ack包和一个新的syn包
-
客户端接受服务端返回的ack和syn包,并对syn返回ack包
- 客户端由从
SYN_SENT切换至ESTABLISHED,该状态表示可以传输数据了。
- 客户端由从
-
服务端收到ACK包,成功建立连接,
accept函数返回出客户端套接字。- 此时服务端状态从
SYN_RCVD切换至ESTABLISHED
- 此时服务端状态从
- 客户请求服务器,服务器回复客户
- 客户收到通知,确认链接建立,回复服务器
- 服务器收到通知,确认链接建立
收发数据
- 连接建立之后,通过客户端套接字收发数据
断开连接:TCP四次握手
- 双方中有一方(假设为A,另一方为B)主动关闭连接(调用
close,或者其进程本身被终止等情况),则其向B发送FIN包 - 此时A从
ESTABLISHED状态切换为FIN_WAIT_1状态 - B接收到FIN包,并发送ACK包
- 此时B从
ESTABLISHED状态切换为CLOSE_WAIT状态 - A接收到ACK包
- 此时A从
FIN_WAIT_1状态切换为FIN_WAIT_2状态 - 一段时间后,B调用自身的
close函数,发送FIN包 - 此时B从
CLOSE_WAIT状态切换为LAST_ACK状态 - A接收到FIN包,并发送ACK包
- 此时A从
FIN_WAIT_2状态切换为TIME_WAIT状态 - B接收到ACK包,关闭连接
- 此时B从
LAST_ACK状态切换为CLOSED状态 - A等待一段时间(两倍的最长生命周期)后,关闭连接
- 此时A从
TIME_WAIT状态切换为CLOSED状态
3. socket API
socket()
- 用于创建套接字描述符 sockfd
1
2
#include <sys/socket.h>
int socket(int family, int type, int protocol);
参数
- family
指明要创建的协议族
1
2
AF_INET ipv4协议
AF_INET6 ipv6协议
- type
指明套接字类型
1
2
3
4
SOCK_STREAM:字节流套接字,适用于TCP或SCTP协议
SOCK_DGRAM:数据报套接字,适用于UDP协议
SOCK_SEQPACKET:有序分组套接字,适用于SCTP协议
SOCK_RAW:原始套接字,适用于绕过传输层直接与网络层协议(IPv4/IPv6)通信
- protocol
该参数用于指定协议类型。
如果是TCP协议的话就填写IPPROTO_TCP,UDP和SCTP协议类似。
也可以直接填写0,这样的话则会默认使用family参数和type参数组合制定的默认协议
(参照上面type参数的适用协议)
返回值
socket函数在成功时会返回套接字描述符,失败则返回-1。
失败的时候可以通过输出errno来详细查看具体错误类型。
errno 类
- 通常一个内核函数运行出错的时候,它会定义全局变量
errno并赋值。 -
引入
sys/errno.h头文件时便可以使用这个变量。并利用这个变量查看具体出错原因。 - 一共有两种查看的方法:
- 直接输出
errno,根据输出的错误码查找解决方案 - 借助
strerror()函数,使用strerror(errno)得到一个具体描述其错误的字符串
- 直接输出
1
2
3
4
5
6
7
8
9
#include <sys/errno.h>
...
// 函数里面直接使用即可
if (-1 == bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
printf("Bind error(%d): %s\n", errno, strerror(errno));
return -1;
}
...
bind()
- 服务端调用
- 绑定 套接字和 ip::port
1
2
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
##### 参数
- 套接字描述符
- 套接字地址结构体
- 套接字地址结构体的长度
套接字地址结构体
1
2
3
4
5
6
#include <sys/socket.h>
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family; // 地址协议族
char sa_data[14]; // 地址数据
};
- 但是我们一般不使用这个结构体,而是使用一个更特定化的结构:IPv4套接字地址结构体
IPv4套接字地址结构体的定义:
1
2
3
4
5
6
7
8
9
10
11
12
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr; // 32位IPv4地址
};
struct sockaddr_in {
uint8_t sin_len; // 结构长度,非必需
sa_family_t sin_family; // 地址族,一般为AF_****格式,常用的是AF_INET
in_port_t sin_port; // 16位TCP或UDP端口号
struct in_addr sin_addr; // 32位IPv4地址
char sin_zero[8]; // 保留数据段,一般置零
};
三个有用字段
- sin_family
- sin_addr
- sin_port
listen()
- 服务端调用
- 开启套接字的监听状态,即将套接字由close转为listen
1
2
#include <sys/socket.h>
int listen(int socket, int backlog);
参数
- sockfd 为要设置的套接字
- backlog 为服务器处于listen状态下要维护的队列长度最大值
backlog参数详解
- 服务器套接字处于
LISTEN状态下所维护的未完成连接队列(SYN队列)和已完成连接队列(Accept队列)的长度和的最大值 - ↑ 这个是原本的意义,现在的
backlog仅指Accept队列的最大长度,SYN队列的最大长度由系统的另一个变量决定 - 这两个队列用于维护与客户端的连接,其中:
- 客户端发送的SYN到达服务器之后,服务端返回SYN/ACK,并将该客户端放置SYN队列中(第一次+第二次握手)
- 当服务端接收到客户端的ACK之后,完成握手,服务端将对应的连接从SYN队列中取出,放入Accept队列,等待服务器中的accept接收并处理其请求(第三次握手)
-
backlog参数大小的选择:
-
backlog是由程序员决定的,不过最后的队列长度其实是min(backlog, /proc/sys/net/core/somaxconn , net.ipv4.tcp_max_syn_backlog ),后者直接读取对应位置文件就有了。不过由于后者是可以修改的,故这里讨论的
backlog实际上是这两个值的最小值。至于如何调参,可以参考这篇博客:
https://ylgrgyq.github.io/2017/05/18/tcp-backlog/
事实上
backlog仅仅是与Accept队列的最大长度相关的参数,实际的队列最大长度视不同的操作系统而定。例如说MacOS上使用传统的Berkeley算法基于backlog参数进行计算,而Linux2.4.7上则是直接等于backlog+3。
-
返回值
- 若成功则返回0,否则返回-1并置相应的
errno。
connect()
- 供客户端调用
- 连接目标是 服务器(绑定了指定IP和port并且处于LISTEN状态的服务器)
函数原型
1
2
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
参数
- 客户端套接字
- 第二个参数为用于指定服务端的ip和port的套接字地址结构体
- 第三个参数为该结构体的长度。
参数的配置与使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 配置
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, kDefaultServerIp.c_str(), &server_addr.sin_addr);
server_addr.sin_port = htons(kDefaultPort);
// 创建链接
if (-1 == connect(sockfd, (struct sockaddr *)(&server_addr),
sizeof(struct sockaddr))) {
fprintf(stderr, "Connect error:%s\n", strerror(errno));
close(sockfd);
sockfd = 0;
return -1;
}
补充说明:IP地址格式转换函数
- IP地址格式
- 表达格式:字符串“172.0.0.1”
- 数值格式:可以存入套接字地址结构体的格式,整型
函数原型
1
2
3
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
参数
inet_pton()函数
- 用于将IP地址从表达格式转换为数值格式
- 第一个参数指定协议族(AF_INET或AF_INET6)
- 第二个参数指定要转换的表达格式的IP地址
- 第三个参数指定用于存储转换结果的指针
- 对于返回结果而言:
- 若转换成功则返回1
- 若表达格式的IP地址格式有误则返回0
- 若出错则返回-1
inet_ntop()函数
- 用于将IP地址从数值格式转换为表达格式
- 第一个参数指定协议族
- 第二个参数指定要转换的数值格式的IP地址
- 第三个参数指定用于存储转换结果的指针
- 第四个参数指定第三个参数指向的空间的大小,用于防止缓存区溢出
-
第四个参数可以使用预设的变量:
1 2 3
#include <netinet/in.h> #define INET_ADDRSTRLEN 16 // IPv4地址的表达格式的长度 #define INET6_ADDRSTRLEN 46 // IPv6地址的表达格式的长度
- 对于返回结果而言
- 若转换成功则返回指向返回结果的指针
- 若出错则返回NULL
返回值
- 若成功则返回0,否则返回-1并置相应的
errno。
connect 异常
connect函数会出错的几种情况:
-
若客户端在发送SYN包之后长时间没有收到响应,则返回
ETIMEOUT错误 -
-
一般而言,如果长时间没有收到响应,客户端会重发SYN包,若超过一定次数重发仍没响应的话则会返回该错误
-
可能的原因是目标服务端的IP地址不存在
-
-
若客户端在发送SYN包之后收到的是RST包的话,则会立刻返回
ECONNREFUSED错误 -
-
当客户端的SYN包到达目标机之后,但目标机的对应端口并没有正在
LISTEN的套接字,那么目标机会发一个RST包给客户端 -
可能的原因是目标服务端没有运行,或者没运行在客户端知道的端口上
-
-
若客户端在发送SYN包的时候在中间的某一台路由器上发生ICMP错误,则会发生
EHOSTUNREACH或ENETUNREACH错误 -
- 事实上跟处理未响应一样,为了排除偶然因素,客户端遇到这个问题的时候会保存内核信息,隔一段时间之后再重发SYN包,在多次发送失败之后才会报错
- 路由器发生ICMP错误的原因是,路由器上根据目标IP查找转发表但查不到针对目标IP应该如何转发,则会发生ICMP错误
- 可能的原因是目标服务端的IP地址不可达,或者路由器配置错误,也有可能是因为电波干扰等随机因素导致数据包错误,进而导致路由无法转发
注意:异常处理
- 由于connect函数在发送SYN包之后就会将自身的套接字从
CLOSED状态置为SYN_SENT状态,故当connect报错之后需要主动将套接字状态置回CLOSED。此时需要通过调用close函数主动关闭套接字实现。 - 故客户端代码需要注意手动关闭套接字:
1
2
3
4
5
6
if (-1 == connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
printf("Connect error(%d): %s\n", errno, strerror(errno));
close(sockfd); // 当connect出错时需要关闭套接字
return -1;
}
accept()
- 服务端调用
- 用于跟客户端建立连接,并返回客户端套接字
- 从Accept队列中pop出一个已完成的连接
- 若Accept队列为空,则accept函数所在的进程阻塞
函数原型
1
2
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
参数
- 第一个参数为服务端自身的套接字
- 第二个参数用于接收客户端的套接字地址结构体
- 第三个参数用于接收第二个参数的结构体的长度
返回值
- 当accept函数成功拿到一个已完成连接时,其会返回该连接对应的客户端套接字描述符,用于后续的数据传输
- 若发生错误则返回-1并置相应的
errno
recv() send()
- recv函数用于通过套接字接收数据
- send函数用于通过套接字发送数据
函数原型
1
2
3
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);
参数
- 第一个参数为要读写的套接字
- 第二个参数指定要接收数据的空间的指针(recv)或要发送的数据(send)
- 第三个参数指定最大读取的字节数(recv)或发送的数据的大小(send)
- 第四个参数用于设置一些参数,默认为0
- 目前用不到第四个参数,故暂时不做展开
返回值
- recv()
- 成功,返回所读取的字节数
- 失败,返回-1,errno
- send()
- 成功,返回写入的字节数
- 失败,返回-1,errno
- 事实上,当返回值与
nbytes不等时,也可以认为其出错
close()
- 用于断开连接,关闭套接字,终止TCP连接
原型
1
2
#include <unistd.h>
int close(int sockfd);
返回值
- 若close成功则返回0
- 否则返回-1并置
errno。
常见的错误
关闭一个无效的套接字
4. Http服务器搭建
将返回按照http协议格式填充即可
1
2
3
4
5
6
7
void setResponse(char *buff) {
bzero(buff, sizeof(buff));
strcat(buff, "HTTP/1.1 200 OK\r\n");
strcat(buff, "Connection: close\r\n");
strcat(buff, "\r\n");
strcat(buff, "<h1>Hello</h1>\n");
}
开启服务之后,可以浏览器访问 http://localhost:16555/
5. 压力测试
- 服务器能力指标
- 在大量请求下仍能正确响应请求
- 大量请求:
- 总的请求数多
- 请求并发量大
- 工具
- Apache Bench
- 模拟大量的HTTP请求,只能测试HTTP服务器
- Apache Bench
Apache Bench
- 模拟大量的HTTP请求,只能测试HTTP服务器
Install
1
sudo apt-get install apache2-utils
命令
1
ab -c 10 -n 10000 http://127.0.0.1:16555/
1
2
-c 请求并发数
-n 总请求量
结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ ab -c 10000 -n 100000 -r "http://192.168.19.12:16555/"
...
Complete requests: 10000 # 总共测试数量
Failed requests: 3403 # 失败数量
(Connect: 0, Receive: 11345, Length: 11345, Exceptions: 11345)
Write errors: 0
Total transferred: 4133096 bytes
HTML transferred: 563604 bytes
Requests per second: 3278.15 [#/sec] (mean)
Time per request: 3050.501 [ms] (mean)
Time per request: 0.305 [ms] (mean, across all concurrent requests)
Transfer rate: 132.31 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 481 1061.9 146 7392
Processing: 31 1730 3976.7 561 15361
Waiting: 0 476 319.3 468 10064
Total: 175 2210 3992.2 781 15361
Percentage of the requests served within a certain time (ms)
50% 781
66% 873
75% 1166
80% 1783
90% 4747
95% 15038
98% 15076
99% 15087
100% 15361 (longest request)
Contact
Feel free to contact me windmillyucong@163.com anytime for anything.