【计算机网络】网络编程套接字
文章目录
- 理解源IP地址和目的IP地址
- 理解端口号和进程ID
- 理解源端口号和目的端口号
- 认识TCP协议
- 认识UDP协议
- 网络字节序
- socket编程接口
- socket网址查看
- socket常见API
- UDP协议实现网络通信
- UDP创建socket文件描述符
- sockaddr结构
- UDP绑定端口号
- UDP接收发送网络数据
- 简单的UDP网络程序
- TCP协议实现网络通信
- TCP创建socket文件描述符
- TCP绑定端口号
- TCP建立连接
- TCP接收请求
- TCP发起连接
- TCP接收发送网络数据
- 简单TCP网络程序
- 多进程优化TCP服务器
- 多线程优化TCP服务器
- 线程池优化TCP服务器
- socket编程总结
- TCP协议三次握手四次挥手
- 学习路线规划
理解源IP地址和目的IP地址
IP地址:公网IP,用于唯一标识互联网中的一台主机
源IP,目的IP:对于一个报文来讲,从哪来,到哪去。
源IP指将数据发送过来的IP地址,目的IP指将数据发送给下一个设备的IP地址(mac地址的变化)
意义: 指导一个报文该如何进行路径选择,目的IP是让我们根据目标进行路径选择的依据
理解端口号和进程ID
IP仅仅是解决了两台物理机器之间的相互通信,但是我们还要考虑如何保证双方的用户之间可以看到发送的和接受的数据。所以网络通信传输数据是使用进程来发送和接受的,pid唯一标识一台主机上的一个进程,port端口号也是唯一标识一台机器上的一个进程, 互联网世界本质上就是一个进程通信的世界
IP + PORT = socket(网络套接字)能够标识互联网中的唯一一个进程
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据需要交给哪一个进程来处理
- IP地址+端口号可以标识网络上某一台主机的某一个进程
- 一个端口号只能被一个进程占用
进程PID vs PORT 为什么PID已经可以标识一台机器上的一个进程了,为啥还要创造PORT端口号呢??
这就像身份证号和学号的关系一样,身份证号和学号都可以唯一标识一名学生。若不使用学号只使用身份证号会出现以下问题:
1、如果身份证号的格式发生变化,那么学校的系统就不能用了
2、身份证号可能并不能标识一个学生在学校里的信息,比如入学年份、学院、专业、班级等,而只需要一个学号就可以获得这些信息,便于筛选查询数据
所以PORT的创建是为了网络部分和操作系统部分进行解耦
另外一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是描述"数据是谁发的,要发给谁"
计算机本身不产生数据,产生数据的是人。人通过特定的客户端产生数据通过网络通信被传送到服务器中。所以所谓的网络通信本质上就是进程间通信 ,比如:抖音的app客户端(进程)<->抖音的服务器(也是一个进程)
认识TCP协议
TCP(Transmission Control Protocol 传输控制协议)特性:
- 传输层协议
- 有连接
- 不可传输
- 面向数据报
认识UDP协议
UDP(USser Datagram Protocol 用户数据报协议)特性:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
TCP 可靠,UDP不可靠。TCP需要使用更多的资源,具体使用哪个必须根据使用场景决定
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为了使网络程序具有可移植性,使用同样的C代码在大端和小端计算机上编译后都能够正常运行,可以调用以下库函数做网络字节序和主机字节序转换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-003WDtyy-1673836199573)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230114135903412.png)]
这些函数名非常好记 h(host 主机) to n(net 网络) l(32位长整型) s(16位短整型)
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数会将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回
socket编程接口
socket网址查看
PING + 网址 (查看IP + PORT)
socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
可以看到bind(), accept(), connect()
函数的参数中都含有一个const struct sockaddr *address
,这是因为网络通信的标准方式有多种,例如基于IP的网络通信,AF_INET,原始套接字,域间套接字等的通信方式。为了系统结构的统一化,程序员们设计出一种结构sockaddr
作为一个参数标识通信方式,让我们可以使用同一个接口来完成通信
UDP协议实现网络通信
UDP创建socket文件描述符
man socket # 查看文档
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
DESCRIPTION
// socket函数创建一个通信端点并且返回一个描述符
socket() creates an endpoint for communication and returns a descriptor.
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in <sys/socket.h>.
// 目前已知的具体格式包含
The currently understood formats include:
Name Purpose Man page
//有点多就不全部复制过来了,最常用的就是这个
AF_INET IPv4 Internet protocols ip(7)
The socket has the indicated type, which specifies the communication semantics. Currently defined types are:
//TCP协议使用 提供有序,可靠,双向,基于连接的字节流,可以支持带外数据传输机制
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
//UDP协议使用 支持数据报(固定最大长度的无连接、不可靠消息)
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
RETURN VALUE
//成功返回文件描述符,若出现错误返回-1,并正确设置errno
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
第一个参数:domain参数指定一个通信域,domain参数选择用于通信的协议族,这些协议族被定义在<sys/socket.h>头文件中
AF_INET IPv4 Internet protocols ip(7)
第二个参数:type 套接字有指定的类型,它指定通信语义,当前定义的类型有(这里也是列举了两个最常用的)
TCP协议使用 提供有序,可靠,双向,基于连接的字节流,可以支持带外数据传输机制
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
UDP协议使用 支持数据报(固定最大长度的无连接、不可靠消息)
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
第三个参数:protocol 指定协议,通常只有一个协议支持特定套接字类型,所以在这种情况下协议可以指定为零
返回值:成功返回一个文件描述符,失败返回-1
示例代码
// 1、创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0){
std::cerr << "socket create errno:" << errno << std::endl;
return 1;
}
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.
-
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.
-
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
-
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
注意:
在socket API中sockaddr和sockaddr_un都可以使用sockaddr_in类型表示,当我们传入sockaddr_in类型的结构体对象指针,接口会拿出前16位地址类型判断使struct sockaddr还是structaddr_un。在使用的时候需要强制转化成sockaddr_in.
这里可能会有小伙伴感觉奇怪,为啥不使用void*传入参数而是从新创建一个结构体sockaddr_in呢?这是因为当时的C语言还并不支持 void *语法,为了方便两种接口的统一就这样设计的
UDP绑定端口号
想要使用绑定端口号函数首先要先初始化填充sockaddr结构体对象,需要填充三个参数
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
第一个参数:socket 一个文件描述符,指定绑定的网络文件
第二个参数:address 一个结构体指针,用于指定协议类型
第三个参数:address_len ssize_t类型的重命名,用于指定address结构体对象的大小
返回值: 成功返回0,失败返回-1
查看address_in 类型的定义,编辑器用的是vscode,右击类型转到定义就可以查看了
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
// 2 、服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET; //sockaddr 初始化1
//此处的端口号,是我们计算机上的变量,是主机序列
local.sin_port = htons(port); //sockaddr 初始化2
//a.需要将人识别的点分十进制,字符串风格的IP地址,转换成4字节整数iP
//b.也要考虑大小端
//云服务器不允许用户直接bind公网IP,另外,实际正常编写的时候也不会指明IP
//local.sin_addr.s_addr = inet_addr("43.2.2.2");
//INADDR_ANY: 如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据才会
//交给你的网络进程,但是一般的服务器右多个网卡,配置多个IP,我们需要的不是某个IP上的数据
//我们需要的是,所有发送到该主机,发送到该端口的数据
local.sin_addr.s_addr = INADDR_ANY;//sockaddr 初始化3
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind errno" << errno << std::endl;
return 2;
}
UDP接收发送网络数据
UDP接受和发送网络数据一般使用这组接口。UDP使用的是数据报格式的数据传输,所以不可以使用字节流式接口
#include <sys/types.h>
#include <sys/socket.h>
// UDP发送网络数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// UDP接受网络数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
recvfrom()
第一个参数: sockfd 网络文件描述符
第二个参数: buf 传输的数据
第三个参数: len 传输数据的大小是多少个字节
第四个参数: flags 设置为0
第五个参数: *dest_addr 输出型参数,是谁给我这个进程发送数据的
第六个参数: addrlen 输出型参数用于标记dest_addr的大小
返回值 成功返回0 失败返回-1
sendto()
第一个参数: sockfd 网络文件描述符
第二个参数: buf 传输的数据
第三个参数: len 传输数据的大小是多少个字节
第四个参数: flags 设置为0
第五个参数: *dest_addr 我需要将这个数据发送给谁
第六个参数: addrlen 数用于标记dest_addr的大小
返回值 成功返回0 失败返回-1
示例代码
// 3、提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while (!quit){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len); //网络中只有数据报和字节流,不需要传送'\0'
if (cnt > 0){
buffer[cnt] = 0;
std::cout << "client# " << buffer << std::endl;
std::string echo_hello = "hello client";
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
}
}
return 0;
}
简单的UDP网络程序
服务器代码
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc){
std::cout << "Usage: " << proc << "port" << std::endl;
}
int main(int argc, char* argv[]){
if (argc != 2){
Usage(argv[0]);
return -1;
}
uint16_t port = atoi(argv[1]);
// 1、创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0){
std::cerr << "socket create errno:" << errno << std::endl;
return 1;
}
// 2 、服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
//此处的端口号,是我们计算机上的变量,是主机序列
local.sin_port = htons(port);
//a.需要将人识别的点分十进制,字符串风格的IP地址,转换成4字节整数iP
//b.也要考虑大小端
//云服务器不允许用户直接bind公网IP,另外,实际正常编写的时候也不会指明IP
//local.sin_addr.s_addr = inet_addr("43.2.2.2");
//INADDR_ANY: 如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据才会
//交给你的网络进程,但是一般的服务器右多个网卡,配置多个IP,我们需要的不是某个IP上的数据
//我们需要的是,所有发送到该主机,发送到该端口的数据
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind errno" << errno << std::endl;
return 2;
}
// 3、提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while (!quit){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if (cnt > 0){
buffer[cnt] = 0;
std::cout << "client# " << buffer << std::endl;
std::string echo_hello = "hello client";
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
}
}
return 0;
}
客户端代码
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc){
std::cout << "Usage:\n\t" << proc << " server_ip server_port" << std::endl;
}
int main(int argc, char* argv[]){
if (argc != 3) {
Usage(argv[0]);
return 3;
}
//1、创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0){
std::cerr << "socket error : " << errno << std::endl;
return 1;
}
//2、客户端也必须显示的bind的吗?
//a、首先,客户端必须也要有ip和port
//b、但是,客户端不需要显示bind!一旦显示bind,就必须明确,client要和哪一个port关联
//client指明端口号,在client一定存在吗??会不会已经被其他人绑定了呢??一旦port被占用则无法使用服务
//server要求port必须明确,并且不变,但client只要有就可以!一般是OS自动给我们bind()
//当client正常发送数据的时候,OS会自动给你bind,采用随机端口的方式,自动帮你匹配合适端口
//2.使用服务
while (true){
std::string message;
std::cout << "输入#";
std::cin >> message;
//a、你的数据从哪里来
//b、你要发给谁
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sock, message.c_str(), message.length(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
int cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len);
if (cnt > 0){
buffer[cnt] = 0;
std::cout << "server echo#" << buffer << std::endl;
}
}
return 0;
}
TCP协议实现网络通信
TCP创建socket文件描述符
客户端服务端都需要
//1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0){
std::cerr << "socket create errno" << errno << std::endl;
return 2;
}
TCP绑定端口号
服务端需要显示绑定,客户端OS自动绑定
//2.bind
uint16_t port = atoi(argv[2]);
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
local.sin_family = AF_INET;
if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind errno" << errno << std::endl;
return 3;
}
TCP建立连接
服务端需要
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog); //第二个参数传5即可
阅读文档
DESCRIPTION //描述
//listen函数所指的sockfd套接字标记为被动套接字,就是这个套接字将使用accept(2)标准接收过来的连接请求
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).
The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.
//backlog参数定义sockfd的挂起连接队列可能增长的最大长度。如果连接请求在队列已满时到达客户端可能会收到带有ECONNREFUSED指示的错误,或者,如果底层协议支持重传,则可以忽略该请求,以便稍后重新尝试连接成功。
The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, theclient may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt
at connection succeeds.
示例代码
//3.因为tcp是面向连接的,所以在通信之前必须要建立连接
//连接一定是有人主动建立(客户端),一定有人被动接收连接(服务器)
const int back_log = 5;
if (listen(sock, back_log) < 0){
std::cerr << "listen error" << std::endl;
return 4;
}
TCP接收请求
服务端需要
上文创建的套接字为监听套接字,用于与端口号绑定。accept()函数用于接收监听套接字获得到的请求,并与其建立连接,会返回一个新的文件描述符用于数据传输
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
阅读文档
accept()系统调用用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。它提取侦听套接字挂起连接队列中的第一个连接请求sockfd,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符。新创建的套接字未处于侦听状态。原始套接字sockfd不受此调用的影响。
The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connec‐tions for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket. The newly created socket is not in the
listening state. The original socket sockfd is unaffected by this call.
示例代码
//accept
for ( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len); //接收请求
if (new_sock < 0){ //如果接收失败则跳过
continue;
}
//提供服务
while (true){
}
}
TCP发起连接
客户端需要
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect errno" << errno << std::endl;
return 3;
}
TCP接收发送网络数据
因为TCP协议使用字节流的流式传输格式,所以也可以使用read(), write(),recv(), send()
等方法流式写入读取的方法进行接收和发送数据
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0){
//将获取的内容当作字符串
buffer[s] = 0;
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<";
echo_string += buffer;
echo_string += "...";
write(sock, echo_string.c_str(), echo_string.size());
}
简单TCP网络程序
经过上面的学习,我们可以将示例代码进行拼接,很轻松就可以写出一个服务器和客户端
服务器代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <string>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void Usage(std::string s){
std::cout << "Usage :" << "\n\t" << s << " port " << std::endl;
}
int main(int argc, char* argv[]){
if (argc != 2){
Usage(argv[0]);
return 1;
}
//1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0){
std::cerr << "socket create errno" << errno << std::endl;
return 2;
}
std::cout << "listen success" << std::endl;
//2.bind
uint16_t port = atoi(argv[1]);
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
local.sin_family = AF_INET;
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind errno" << errno << std::endl;
return 3;
}
std::cout << "bind success" << std::endl;
//3.因为tcp是面向连接的,所以在通信之前必须要建立连接
//连接一定是有人主动建立(客户端),一定有人被动接收连接(服务器)
const int back_log = 5;
if (listen(listen_sock, back_log) < 0){
std::cerr << "listen error" << std::endl;
return 4;
}
//accept
for ( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (new_sock < 0){
continue;
}
std::cout << "get a link ..." << std::endl;
//提供服务
while (true){
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0){
//将获取的内容当作字符串
buffer[s] = 0;
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<";
echo_string += buffer;
echo_string += "...";
write(sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0){
std::cout << "client quit ..." << std::endl;
break;
}
else {
std::cerr << "read errno" << errno << std::endl;
break;
}
}
}
}
客户端代码
#include <iostream>
#include <stdlib.h>
#include <cerrno>
#include <cstring>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void Usage(std::string s){
std::cout << "Usage :" << "\n\t" << s << " server_ip server_port " << std::endl;
}
int main(int argc, char* argv[]){
if (argc != 3){
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0){
std::cerr << "socket errno" << errno << std::endl;
return 2;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect errno" << errno << std::endl;
return 3;
}
//进行正常的业务请求
while (true){
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer) - 1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0){
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
}
但是以上代码有一个非常明显的问题,那就是一个服务器在接收一个客户端的连接后为这个客户端提供服务,而主进程就死循环执行服务代码了,无法继续接收其他客户端的连接,导致我们的服务器在同一时刻只能服务一个客户端,接下来我们会使用操作系统的知识对服务器进行优化
多进程优化TCP服务器
我们可以让服务器主进程接收客户端的连接,然后创建子进程来为客户端提供服务。
方法一 在信号章节我们学过signal()
函数,其可以自定义信号的处理方式
#include <signal.h>
signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHILD信号,子进程会自动退出释放资源
在子进程退出时会给父进程发送SIGCHILD信号,告诉父进程我推出了,如果我们让父进程忽略SIGCHILD信号,那么子进程就会自动退出并且释放资源,父进程就无需等待
方法二 让父进程进行waitpid()
,子进程马上退出,让孙子进程执行服务,孙子进程会被操作系统领养
//child
if (fork() > 0) exit(0);
close(listen_sock);
ServiceIO(new_sock); //提供服务
close(new_sock);
exit(0);
所以我们可以将提供的服务放在一个函数中供子进程调用
signal(SIGCHLD, SIG_IGN); // 在Linux中父进程忽略子进程的SIGCHILD信号,子进程会自动退出释放资源
//accept
for ( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (new_sock < 0){
continue;
}
std::cout << "get a link ..." << std::endl;
pid_t id = fork();
if (id < 0){
continue;
}
else if (id == 0){
//child
if (fork() > 0) exit(0);
close(listen_sock);
ServiceIO(new_sock); //提供服务
close(new_sock);
exit(0);
}
else {
//father
close(new_sock);
}
}
子进程会继承父进程的struct files_struct
因为子进程只提供服务所以listen_sock
是没有用的,可以将其关掉,对于父进程只用于监听接收请求,所以提供服务的new_sock
传递给子进程后就没有用了,可以关闭,防止文件描述符泄露
多线程优化TCP服务器
创建线程
pthread_t tid;
int *pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest, pram);
线程执行函数
void *HandlerRequest(void* args){
pthread_detach(pthread_self());
int sock = *(int*)args;
delete (int*)args;
ServiceIO(sock); //提供服务
close(sock);
}
注意:该代码存在以下问题
a、创建线程、进程无上限
b、当客户连接来了,我们才给客户创建进程/线程
线程池优化TCP服务器
使用find()
指令查找我们之前写过的线程池
[clx@VM-20-6-centos Lesson_Linux]$ ll
total 28
drwxrwxr-x 21 clx clx 4096 Aug 1 00:49 21_7_lesson
drwxrwxr-x 25 clx clx 4096 Aug 27 09:40 21_8_lesson
drwxrwxr-x 21 clx clx 4096 Oct 30 20:28 22_10_lesson
drwxrwxr-x 23 clx clx 4096 Nov 28 19:13 22_11_lesson
drwxrwxr-x 6 clx clx 4096 Dec 30 21:09 22_12_lesson
drwxrwxr-x 18 clx clx 4096 Sep 30 18:01 22_9_lesson
drwxrwxr-x 5 clx clx 4096 Jan 16 09:01 23_1_lesson
[clx@VM-20-6-centos Lesson_Linux]$ find . -name ThreadPool.hpp //查找线程池
./22_11_lesson/lesson_11_24/FirstProject/ThreadPool.hpp
./22_11_lesson/lesson_11_25/FirstProject/ThreadPool.hpp
创建服务套接字后将使用套接字创建一个任务,然后Push到线程池里就可以了
Task t(new_sock);
ThreadPool::GetInstance()->InitThreadPool();
ThreadPool::GetInstance()->PushTask(new_sock);
Task代码
#pragma once
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
class Task{
public:
Task() : sock(-1){};
Task(int _sock) : sock(_sock){}
void ProcessOn(){
// while (true){
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0){
//将获取的内容当作字符串
buffer[s] = 0;
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<";
echo_string += buffer;
echo_string += "...";
write(sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0){
std::cout << "client quit ..." << std::endl;
//break;
}
else {
std::cerr << "read errno" << errno << std::endl;
//break;
}
//}
close(sock);
}
void operator()(){
ProcessOn();
}
private:
int sock;
};
线程池代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <queue>
#include "Task.hpp"
#define THREAD_NUM 6
class ThreadPool
{
private:
ThreadPool(int num = THREAD_NUM) : _pthread_num(num), _stop(false)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_cond, nullptr);
}
public:
~ThreadPool()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
static void *ThreadRoutine(void *args);
static ThreadPool *GetInstance();
bool InitThreadPool();
void PushTask(const Task &task);
void PopTask(Task &task);
void ThreadWait() { pthread_cond_wait(&_cond, &_lock); }
void ThreadWakeUp() { pthread_cond_signal(&_cond); }
void ThreadLock() { pthread_mutex_lock(&_lock); };
void ThreadUnlock() { pthread_mutex_unlock(&_lock); };
bool IsStop() { return _stop; }
bool TaskQueueIsEmpty() { return _task_queue.empty(); }
private:
std::queue<Task> _task_queue;
size_t _pthread_num;
bool _stop;
pthread_mutex_t _lock;
pthread_cond_t _cond;
static ThreadPool *_single_instance;
};
ThreadPool *ThreadPool::_single_instance = nullptr;
ThreadPool *ThreadPool::GetInstance()
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
if (_single_instance == nullptr)
{
pthread_mutex_lock(&mutex);
_single_instance = new ThreadPool();
_single_instance->InitThreadPool();
pthread_mutex_unlock(&mutex);
}
return _single_instance;
}
bool ThreadPool::InitThreadPool()
{
pthread_t tid;
for (size_t i = 0; i < _pthread_num; i++)
{
int ret = pthread_create(&tid, nullptr, ThreadRoutine, (void *)this);
if (ret != 0)
{
std::cout << "pthread_create errno" << std::endl;
return false;
}
}
std::cout << "InitThreadPool success" << std::endl;
return true;
}
void *ThreadPool::ThreadRoutine(void *args)
{
ThreadPool *tp = (ThreadPool *)args;
while (true)
{
Task task(0);
tp->ThreadLock();
while (tp->TaskQueueIsEmpty())
{
tp->ThreadWait();
}
tp->PopTask(task);
tp->ThreadUnlock();
task.ProcessOn();
}
}
void ThreadPool::PushTask(const Task &task)
{
ThreadLock();
_task_queue.push(task);
ThreadWakeUp();
ThreadUnlock();
}
void ThreadPool::PopTask(Task &task)
{
task = _task_queue.front();
_task_queue.pop();
}
socket编程总结
- 如果不关闭不需要的文件描述符,会造成文件描述符泄露
- 创建socket的过程,socket()本质是打开文件 – 仅仅有系统相关的内容
- bind(), struct sockaddr_in -> ip, port 本质使用ip + port 和文件信息进行关联
- listen(), 本质是设置该socket文件的状态,允许别人来连接我
- accept(), 获取新链接到应用层,是以fd为代表的。当有很多链接连上我们的服务器的时候,OS中会存在大量链接。OS许哟啊管理这些建立好的链接,如何管理??先描述再阻止
- 所谓的链接,在OS层面上本质就是一个描述链接的结构体(文件)
- read/write,本质就是进行网络通信,但是对于用户来讲,相当于文件读写
- close(fd)关闭文件 a.系统层面,释放曾经申请的文件资源,链接资源等 b.网络层面,通知对方,我的丽娜姐已经关闭了
- connect(),本质就是发起连接,在系统层面,就是构建一个请求报文发送过去,在网络层面,发起TCP连接的三次握手
- close(), client 和 server都需要执行,本质在网络层面,其实就是在进行第四次挥手
TCP协议三次握手四次挥手
服务器初始化:
-
调用socket, 创建文件描述符;
-
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失
败;
-
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
-
调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
-
调用socket, 创建文件描述符;
-
调用connect, 向服务器发起连接请求;
-
connect会发出SYN段并阻塞等待服务器应答; (第一次)
-
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
-
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
-
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
-
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
-
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
-
客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
这个断开连接的过程, 通常称为四次挥手
-
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
-
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
-
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
-
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
在学习 socket API 时需要注意应用程序和TCP协议层是如何交互的:
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些字段,再比如read()返回0就表明收到了FIN段
学习路线规划
现在我们从零开始,通过系统调用接口(socket, bind, listen …)来编写应用层,在完善应用层的基础上之后我们会学习操作系统的传输层和网络层,还有网卡驱动中的数字链路层的原理以及设计
接请求;