【网络】套接字编程——TCP通信
前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!二。
> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:TCP网络服务器简单模拟实现。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:网络
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、前言
前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!
二、主体
学习【网络】套接字编程——TCP通信咱们按照下面的图解:
2.1 程序结构
分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于 echo
指令:
这个程序我们已经基于 UDP
协议实现过了,换成 TCP
协议实现时,程序的结构是没有变化的,同样需要 server.hpp
、server.cc
、client.hpp
、client.cc
这几个文件:
创建
server.hpp
服务器头文件:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip ="0.0.0.0";//默认IP
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
class TcpServer
{
public:
TcpServer( const uint16_t port = default_port,const std::string ip = default_ip)
:ip_(ip),port_(port)
{}
~TcpServer()
{}
// 初始化服务器
void InitServer()
{}
// 启动服务器
void StartServer()
{}
private:
int sock_; // 套接字(存疑)
uint16_t port_; // 端口号
std::string ip_;//ip地址
};
}
创建
server.cc
服务器源文件:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
using namespace nt_server;
//业务处理函数
std::string ExecCommand(const std::string& request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<TcpServer> usvr (new TcpServer(port));
usvr->InitServer();
usvr->StartServer();
return 0;
}
创建
client.hpp
客户端头文件:
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
namespace nt_client
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
class TcpClient
{
public:
TcpClient(const std::string& ip, const uint16_t port)
:server_ip_(ip), server_port_(port)
{}
~TcpClient()
{}
// 初始化客户端
void InitClient()
{}
// 启动客户端
void StartClient()
{}
private:
int sock_; // 套接字
std::string server_ip_; // 服务器IP
uint16_t server_port_; // 服务器端口号
};
}
创建
client.cc
客户端源文件:
#include <memory>
#include "client.hpp"
using namespace std;
using namespace nt_client;
void Usage(const char *program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerIP ServerPort" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
// 服务器IP与端口号
string ip(argv[1]);
uint16_t port = stoi(argv[2]);
unique_ptr<TcpClient> usvr(new TcpClient(ip, port));
usvr->InitClient();
usvr->StartClient();
return 0;
}
创建
Makefile
文件:
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
2.2 Tcp Server 端代码
2.2.1 socket、bind - 初始化服务端
说明:
在使用 socket 函数创建套接字时,UDP 协议需要指定参数2为 SOCK_DGRAM,TCP 协议则是指定参数2为 SOCK_STREAM。
代码呈现:
server.hpp的初始化部分:
// 初始化服务器
void InitServer()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if(bind(sock_,(const sockaddr *)&local, sizeof(local))<0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
// 3.TODO
}
解释说明:
- 在绑定端口号时,一定需要把主机序列转换为网络序列。
- 发送信息阶段,recvfrom / sendto 等函数会自动将需要发送的信息转换为网络序列,接收信息时同样会将其转换为主机序列,所以不需要手动转换。
总结:
TCP是面向连接的,服务器一般是比较被动的,没人访问,这个服务器只能干等着,而且也不能退出。就像你是一家餐馆的老板,你只能在餐馆里被动的等待顾客的到来,顾客什么时候来你也不知道。服务器要一直要处于等待链接到来的状态——监听状态。
2.2.2 listen - 监听一个套接字
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
使用说明:
listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接)。所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
参数说明:
- int sockfd:服务端的socket,也就是socket函数创建的,标识绑定的,未连接的套接字的描述符。
- int backlog:backlog 为请求队列的最大长度。
细节说明:
listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
代码呈现:
server.hpp的初始化服务器部分:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog=5;//请求队列的最大长度
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer
{
public:
TcpServer( const uint16_t port = default_port,const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if(bind(sock_,(const sockaddr *)&local, sizeof(local))<0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if(listen(sock_,backlog)<0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
for(;;)
{
std::cout<<"TCP SERVER is running....."<<std::endl;
sleep(1);
}
}
private:
int sock_; // 套接字(存疑)
uint16_t port_; // 端口号
std::string ip_; // ip地址
};
}
2.2.3 accept - 获取一个新连接
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
使用说明:
- accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
- accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sockfd 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
参数说明:
- sockfd:为服务器端套接字。
- addr:sockaddr_in 结构体变量。
- addrlen:参数 addr 的长度,可由 sizeof() 求得。
addr
与addrlen
:是一个 输入输出型 参数,类似于recvfrom
中的参数。
基于TCP连接的服务器端为什么需要用两个套接字:
- 在服务器端,socket()返回的套接字用于监听(listen)和接受(accept)客户端的连接请求。这个套接字不能用于与客户端之间发送和接收数据。
- 当某个客户端断开连接、或者是与某个客户端的通信完成之后,服务器端需要关闭用于与该客户端通信的套接字。
代码呈现:
server.hpp的StartServer的内容:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<unistd.h>
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog=5;//请求队列的最大长度
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer
{
public:
TcpServer( const uint16_t port = default_port,const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if(bind(listen_sock_,(const sockaddr *)&local, sizeof(local))<0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if(listen(listen_sock_,backlog)<0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
for(;;)
{
//1.获取新连接
struct sockaddr_in client;
socklen_t len=sizeof(client);
int accept_socket=accept(listen_sock_,(struct sockaddr*)&client,&len);
if(accept_socket<0)
{
std::cout<<"accept failed"<<std::endl;
continue;
}
std::cout<<"get a new link...,sockfd:"<<accept_socket<<std::endl;
//2.根据新连接来进行通信
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
};
}
测试工具-->telnet:
安装:
sudo yum install telnet
功能说明:
- telnet是一种用于远程登录的网络协议,可以将本地计算机链接到远程主机。
- Linux提供了telnet命令,它允许用户在本地计算机上通过telnet协议连接到远程主机,并执行各种操作。
- 使用telnet命令可以建立客户端与服务器之间的虚拟终端连接,这使得用户可以通过网络远程登录到其他计算机,并在远程计算机上执行命令,就像直接在本地计算机上操作一样。
使用说明:
telnet [选项] [主机名或IP地址] [端口号]
参数说明:
- -l 用户名:指定用户名进行登录。
- -p 端口号:指定要连接的远程主机端口号。
- -E:在telnet会话中打开字符转义模式。
- -e 字符:指定telnet会话中的转义字符。
- -r:在执行用户登录之前不要求用户名。
- -K:在连接到telnet会话时要求密码。
2.2.4 read - 从套接字中读取数据
功能说明:
- 因为
TCP
是面向字节流的,所以可以直接使用read
系统调用去读取数据。 - 如果客户端退出了,那么
read
会读到0,此时需要把之前accept
返回的sockfd
关闭,防止误操作,造成意想不到的结果。
2.2.5 write - 向套接字中进行写入
使用:
同理,向套接字中进行写入时,直接使用 write 系统调用即可。服务端在收到客户端的数据后,先进行加工处理,然后再进行写入,上面 if(n > 0) 后面就是写入的代码。唯一需要注意的就是,如果在写入前或者正在写入的过程中,client 端退出了,此时客户端与服务器之间的连接就断了,此时客户端如果进行写入操作可能会导致整个服务端崩掉。这和管道类似,读端关闭,写端继续写,操作系统会给写端发送 13 号信号,将写端 kill 调,为了避免这种情况,我们需要在服务器启动的时候将 13 号新号进行捕捉。
2.2.6 总代码呈现
server.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog=5;//请求队列的最大长度
using func_t = std::function<std::string(std::string)>; // 回调函数类型
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer
{
public:
TcpServer(const func_t& func,const uint16_t port = default_port,const std::string ip = default_ip)
: ip_(ip), port_(port),func_(func)//注意这里要传1个业务处理函数
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if(bind(listen_sock_,(const sockaddr *)&local, sizeof(local))<0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if(listen(listen_sock_,backlog)<0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
for(;;)
{
//1.获取新连接
struct sockaddr_in client;
socklen_t len=sizeof(client);
int accept_socket=accept(listen_sock_,(struct sockaddr*)&client,&len);
if(accept_socket<0)
{
std::cout<<"accept failed"<<std::endl;
continue;
}
//2.业务处理
//2.1客户端信息存储
uint16_t clientport=ntohs(client.sin_port);//客户端端口号
char iptr[32];
inet_ntop(AF_INET,&(client.sin_addr),iptr,sizeof(iptr));//客户端IP
std::cout << "Server accept " << iptr << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
//2.2.业务处理
Service(accept_socket,iptr,clientport);
}
}
// 通信服务+业务处理
void Service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buff[1024];
std::string who = clientip + "-" + std::to_string(clientport);
while (true)
{
ssize_t n = read(sock, buff, sizeof(buff) - 1); // 预留 '\0' 的位置
if (n > 0)
{
// 读取成功
buff[n] = '\0';
std::cout << "Server get: " << buff << " from " << who << std::endl;
std::string respond = func_(buff); // 实际业务处理由上层指定
// 发送给服务器
write(sock, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "Client " << who << " " << sock << " quit!" << std::endl;
close(sock); // 关闭文件描述符
break;
}
else
{
// 读取出问题(暂时)
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sock); // 关闭文件描述符
break;
}
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
func_t func_;//业务处理函数
};
}
server.cc代码:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
using namespace nt_server;
// 业务处理回调函数(字符串回响)
string echo(string request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<TcpServer> usvr (new TcpServer(echo,port));
usvr->InitServer();
usvr->StartServer();
return 0;
}
2.3 Tcp Client 端代码
功能说明:
client.cc代码:
#include <iostream>
#include <memory>
#include "client.hpp"
using namespace std;
using namespace nt_client;
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerIP ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
std::string ip=argv[1];
uint16_t port = stoi(argv[2]);//将字符串转换成端口号
unique_ptr<TcpClient> usvr (new TcpClient(ip,port));
usvr->InitClient();
usvr->StartClient();
return 0;
}
2.3.1 socket - 初始化客户端
说明:
- 对于客户端来说,服务器的 IP 地址与端口号是两个不可或缺的元素,因此在客户端类中,server_ip 和 server_port 这两个成员是少不了的,当然得有 socket 套接字。
- 初始化客户端只需要干一件事:创建套接字,客户端是主动发起连接请求的一方,也就意味着它不需要使用 listen 函数设置为监听状态。
- TCP版本是不需要我们手动写代码去bind的,也是操作系统自己去自动bind的。
代码呈现:
client.hpp
客户端头文件:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
namespace nt_client
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpClient
{
public:
TcpClient(const std::string& ip, const uint16_t port)
:server_ip_(ip), server_port_(port)
{}
~TcpClient()
{}
// 初始化客户端
void InitClient()
{
// 创建套接字
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Sock Succeess! " << sock_ << std::endl;
}
// 启动客户端
void StartClient()
{}
private:
int sock_; // 套接字
std::string server_ip_; // 服务器IP
uint16_t server_port_; // 服务器端口号
};
}
2.3.2 connect - 向服务端发起连接
语法:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- int sockdf:socket文件描述符
- const struct sockaddr *addr:传入参数,指定服务器端地址信息,含IP地址和端口号
- socklen_t addrlen:传入参数,传入sizeof(addr)大小
- 返回值:成功为 0,失败为 -1。
代码呈现:
// 启动客户端
void StartClient()
{
// 填充服务器的 sockaddr_in 结构体信息
struct sockaddr_in server;
socklen_t len = sizeof(server);
memset(&server, 0, len);
server.sin_family = AF_INET;
inet_aton(server_ip_.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
server.sin_port = htons(server_port_);
// 尝试重连 5 次
int n = 5;
while(n)
{
int ret = connect(sock_, (const struct sockaddr*)&server, len);
if(ret == 0)
{
// 连接成功,可以跳出循环
break;
}
// 尝试进行重连
std::cerr << "网络异常,正在进行重连... 剩余连接次数: " << --n << std::endl;
sleep(1);
}
// 如果剩余重连次数为 0,证明连接失败
if(n == 0)
{
std::cerr << "连接失败! " << strerror(errno) << std::endl;
close(sock_);
exit(CONNECT_ERR);
}
// 连接成功
std::cout << "连接成功!" << std::endl;
// 进行业务处理
// GetService();
}
2.3.3 write、read - 向服务器发送数据、从服务器接收数据
和服务端一样,客户端也是通过 write 和 read 接口来发送数据和读取数据。
2.3.4 代码呈现
client.hpp代码:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
namespace nt_client
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR,
CONNECT_ERR
};
class TcpClient
{
public:
TcpClient(const std::string& ip, const uint16_t port)
:server_ip_(ip), server_port_(port)
{}
~TcpClient()
{}
// 初始化客户端
void InitClient()
{
// 创建套接字
sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Sock Succeess! " << sock_ << std::endl;
}
// 启动客户端
void StartClient()
{
// 填充服务器的 sockaddr_in 结构体信息
struct sockaddr_in server;
socklen_t len = sizeof(server);
memset(&server, 0, len);//清空
server.sin_family = AF_INET;//网络通信
inet_aton(server_ip_.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
server.sin_port = htons(server_port_);
// 尝试重连 5 次
int n = 5;
while (n)
{
int ret = connect(sock_, (const struct sockaddr *)&server, len);
if (ret == 0)
{
// 连接成功,可以跳出循环
break;
}
// 尝试进行重连
std::cerr << "网络异常,正在进行重连... 剩余连接次数: " << --n << std::endl;
sleep(1);
}
// 如果剩余重连次数为 0,证明连接失败
if (n == 0)
{
std::cerr << "连接失败! " << strerror(errno) << std::endl;
close(sock_);
exit(CONNECT_ERR);
}
// 连接成功
std::cout << "连接成功!" << std::endl;
// 获取服务
GetService();
}
// 获取服务
void GetService()
{
char buff[1024];
std::string who = server_ip_ + "-" + std::to_string(server_port_);
while (true)
{
// 由用户输入信息
std::string msg;
std::cout << "Please Enter >> ";
std::getline(std::cin, msg);
// 发送信息给服务器
write(sock_, msg.c_str(), msg.size());
// 接收来自服务器的信息
ssize_t n = read(sock_, buff, sizeof(buff) - 1);
if (n > 0)
{
// 正常通信
buff[n] = '\0';
std::cout << "Client get: " << buff << " from " << who << std::endl;
}
else if (n == 0)
{
// 读取到文件末尾(服务器关闭了)
std::cout << "Server " << who << " quit!" << std::endl;
close(sock_); // 关闭文件描述符
break;
}
else
{
// 读取异常
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sock_); // 关闭文件描述符
break;
}
}
}
private:
int sock_; // 套接字
std::string server_ip_; // 服务器IP
uint16_t server_port_; // 服务器端口号
};
}
client.cc代码:
#include <iostream>
#include <memory>
#include "client.hpp"
using namespace std;
using namespace nt_client;
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerIP ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
std::string ip=argv[1];
uint16_t port = stoi(argv[2]);//将字符串转换成端口号
unique_ptr<TcpClient> usvr (new TcpClient(ip,port));
usvr->InitClient();
usvr->StartClient();
return 0;
}
2.4 多进程版服务器
2.4.1 父进程阻塞等待
功能阐述:
当服务器成功处理连接请求后,fork 新建一个子进程,用于进行业务处理,原来的进程专注于处理连接请求,子进程创建成功后,会继承父进程的文件描述符表,能轻而易举的获取客户端的 socket 套接字,从而进行网络通信,当然不止文件描述符表,得益于 写时拷贝 机制,子进程还会共享父进程的变量,当发生修改行为时,才会自己创建。
代码呈现:
// 进程创建、等待所需要的头文件
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
// 启动服务器
void StartServer()
{
for (;;)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int accept_socket = accept(listen_sock_, (struct sockaddr *)&client, &len);
if (accept_socket < 0)
{
std::cout << "accept failed" << std::endl;
continue;
}
// 2.业务处理
// 2.1客户端信息存储
uint16_t clientport = ntohs(client.sin_port); // 客户端端口号
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 客户端IP
std::cout << "Server accept " << clientip << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
// 3.创建子进程
pid_t id = fork();
if (id < 0)
{
// 创建子进程失败,暂时不与当前客户端建立通信会话
close(accept_socket);
std::cerr << "Fork Fail!" << std::endl;
}
else if (id == 0)
{
// 子进程内
close(listen_sock_); // 子进程不需要监听(建议关闭)
// 执行业务处理函数
Service(accept_socket, clientip, clientport);
exit(0); // 子进程退出
}
else//父进程
{
// 父进程需要等待子进程
pid_t ret = waitpid(id, nullptr, 0); // 默认为阻塞式等待
if (ret == id)
std::cout << "Wait " << id << " success!";
}
}
}
细节说明:
虽然此时成功创建了子进程,但父进程(处理连接请求)仍然需要等待子进程退出后,才能继续运行,说白了就是 父进程现在处于阻塞等待状态,因此父进程应该需要设置为 非阻塞等待。
2.4.2 非阻塞等待版本
想让父进程不阻塞等待,方法如下:
- 通过参数设置为非阻塞等待(不推荐)
- 设置
SIGCHLD
信号的处理动作为子进程回收(不是很推荐) - 忽略
SIGCHLD
信号(推荐使用) - 设置孙子进程(不是很推荐)
2.4.3 代码呈现
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h> // 信号处理相关头文件
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog = 5; // 请求队列的最大长度
using func_t = std::function<std::string(std::string)>; // 回调函数类型
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer
{
public:
TcpServer(const func_t &func, const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port), func_(func) // 注意这里要传1个业务处理函数
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if (bind(listen_sock_, (const sockaddr *)&local, sizeof(local)) < 0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if (listen(listen_sock_, backlog) < 0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
// 忽略 SIGCHLD 信号
signal(SIGCHLD, SIG_IGN);
for (;;)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int accept_socket = accept(listen_sock_, (struct sockaddr *)&client, &len);
if (accept_socket < 0)
{
std::cout << "accept failed" << std::endl;
continue;
}
// 2.业务处理
// 2.1客户端信息存储
uint16_t clientport = ntohs(client.sin_port); // 客户端端口号
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 客户端IP
std::cout << "Server accept " << clientip << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
// 3.创建子进程
pid_t id = fork();
if (id < 0)
{
// 创建子进程失败,暂时不与当前客户端建立通信会话
close(accept_socket);
std::cerr << "Fork Fail!" << std::endl;
}
else if (id == 0)
{
// 子进程内
close(listen_sock_); // 子进程不需要监听(建议关闭)
// 执行业务处理函数
Service(accept_socket, clientip, clientport);
exit(0); // 子进程退出
}
close(accept_socket); // 父进程不再需要资源(建议关闭)
}
}
// 通信服务+业务处理
void Service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buff[1024];
std::string who = clientip + "-" + std::to_string(clientport);
while (true)
{
ssize_t n = read(sock, buff, sizeof(buff) - 1); // 预留 '\0' 的位置
if (n > 0)
{
// 读取成功
buff[n] = '\0';
std::cout << "Server get: " << buff << " from " << who << std::endl;
std::string respond = func_(buff); // 实际业务处理由上层指定
// 发送给服务器
write(sock, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "Client " << who << " " << sock << " quit!" << std::endl;
close(sock); // 关闭文件描述符
break;
}
else
{
// 读取出问题(暂时)
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sock); // 关闭文件描述符
break;
}
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
func_t func_; // 业务处理函数
};
}
2.5 多线程版本服务器
从内核的观点看:
- 进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。
- 线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
2.5.1 使用原生线程库
概念:
由于我们创建线程是用来提供服务的,而服务端的业务中有一个Service(),它需要我们的线程去传入 Service() 函数中的所有参数,同时也需要具备调用 Service() 业务处理函数的能力,我们只能把Service() 函数中的所有参数和this指针传进去,而这单凭一个 void* 的参数是无法解决的,为此我们可以创建一个类,里面可以包含我们所需要的参数——Service() 函数中的所有参数和this指针。
分析:
所以接下来我们需要在连接成功后,创建次线程,利用已有信息构建 ThreadData 对象,为次线程编写回调函数(最终目的是为了执行 Service() 函数)。
代码呈现:
server.hpp代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h> // 信号处理相关头文件
#include<pthread.h>
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog = 5; // 请求队列的最大长度
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer; // 前置声明
// 包含我们所需参数的类型
class ThreadData
{
public:
ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr)
:sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
{}
// 设置为公有是为了方便访问
public:
int sock_;
std::string clientip_;
uint16_t clientport_;
TcpServer* current_; // 指向 TcpServer 对象的指针
};
using func_t = std::function<std::string(std::string)>; // 回调函数类型
class TcpServer
{
public:
TcpServer(const func_t &func, const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port), func_(func) // 注意这里要传1个业务处理函数
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if (bind(listen_sock_, (const sockaddr *)&local, sizeof(local)) < 0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if (listen(listen_sock_, backlog) < 0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
// 忽略 SIGCHLD 信号
signal(SIGCHLD, SIG_IGN);
for (;;)
{
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int accept_socket = accept(listen_sock_, (struct sockaddr *)&client, &len);
if (accept_socket < 0)
{
std::cout << "accept failed" << std::endl;
continue;
}
// 2.业务处理
// 2.1客户端信息存储
uint16_t clientport = ntohs(client.sin_port); // 客户端端口号
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 客户端IP
std::cout << "Server accept " << clientip << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
// 3.创建线程及所需要的线程信息类
ThreadData* td = new ThreadData(accept_socket, clientip, clientport, this);
pthread_t p;
pthread_create(&p, nullptr, Routine, td);
}
}
// 线程回调函数
static void* Routine(void* args)
{
// 线程分离
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
// 调用业务处理函数
td->current_->Service(td->sock_, td->clientip_, td->clientport_);
// 销毁对象
delete td;
return (void*)0;
}
// 通信服务+业务处理
void Service(int sock, const std::string &clientip, const uint16_t &clientport)
{
char buff[1024];
std::string who = clientip + "-" + std::to_string(clientport);
while (true)
{
ssize_t n = read(sock, buff, sizeof(buff) - 1); // 预留 '\0' 的位置
if (n > 0)
{
// 读取成功
buff[n] = '\0';
std::cout << "Server get: " << buff << " from " << who << std::endl;
std::string respond = func_(buff); // 实际业务处理由上层指定
// 发送给服务器
write(sock, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "Client " << who << " " << sock << " quit!" << std::endl;
close(sock); // 关闭文件描述符
break;
}
else
{
// 读取出问题(暂时)
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sock); // 关闭文件描述符
break;
}
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
func_t func_; // 业务处理函数
};
}
makefile代码呈现:
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11 -lpthread
client:client.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf server client
2.5.2 线程池版本
问题分析:
如果每来一个用户我们就得创建一个线程,那么当来了很多用户,就会消耗很多资源。我们不想等到客户来了才创建我们的线程,我们可以提前创建好,我们不提供死循环服务,为此可以改用之前实现的线程池。
代码呈现:
ThreadPool.hpp代码:
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
// 线程信息结构体
struct ThreadInfo
{
pthread_t tid; // 线程ID
std::string name; // 线程名称
};
// 默认线程数量
static const int defalutnum = 5;
// 线程池模板类
template <class T>
class ThreadPool
{
private:
// 互斥锁加锁函数
void Lock()
{
pthread_mutex_lock(&mutex_);
}
// 互斥锁解锁函数
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
// 唤醒等待的线程
void Wakeup()
{
pthread_cond_signal(&cond_);
}
// 线程休眠等待条件变量
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
// 判断任务队列是否为空
bool IsQueueEmpty()
{
return tasks_.empty();
}
// 根据线程ID获取线程名称
std::string GetThreadName(pthread_t tid)
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
// 线程处理任务的函数
static void *HandlerTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t.Run();
}
}
// 启动线程池中的所有线程
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
}
}
// 从任务队列中取出一个任务
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
// 向任务队列中添加一个任务
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
// 获取线程池单例对象
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // 如果线程池对象不存在,则创建一个新的线程池对象
{
pthread_mutex_lock(&lock_); // 加锁保证线程安全
if (nullptr == tp_) // 再次检查是否已经创建了线程池对象,防止多线程环境下的竞争条件
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>(); // 创建线程池对象
}
pthread_mutex_unlock(&lock_); // 解锁
}
return tp_; // 返回线程池对象指针
}
private:
// 构造函数,初始化线程池,可以指定线程数量,默认为defalutnum
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr); // 初始化互斥锁
pthread_cond_init(&cond_, nullptr); // 初始化条件变量
}
// 析构函数,销毁线程池资源
~ThreadPool()
{
pthread_mutex_destroy(&mutex_); // 销毁互斥锁
pthread_cond_destroy(&cond_); // 销毁条件变量
}
// 禁止拷贝构造和赋值操作符,确保线程池对象的单一性
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
// 线程信息列表
std::vector<ThreadInfo> threads_;
// 任务队列
std::queue<T> tasks_;
// 互斥锁和条件变量用于同步和通信
pthread_mutex_t mutex_;
pthread_cond_t cond_;
// 线程池单例对象指针和互斥锁静态成员变量
static ThreadPool<T> *tp_;
static pthread_mutex_t lock_;
};
// 初始化线程池单例对象指针和互斥锁静态成员变量
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
Task.hpp代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include<pthread.h>
using func_t = std::function<std::string(std::string)>; // 回调函数类型
// 任务类
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport,const func_t &func)
: sockfd_(sockfd), clientip_(clientip), clientport_(clientport), func_2(func) // 注意这里要传1个业务处理函数
{
}
~Task()
{
}
void Run()
{
char buff[1024];
std::string who = clientip_ + "-" + std::to_string(clientport_);
//while (true),如果线程池的线程一直循环在干这件事的话,效率会极其低下
ssize_t n = read((sockfd_),buff, sizeof(buff)- 1); // 预留 '\0' 的位置
if (n > 0)
{
// 读取成功
buff[n] = '\0';
std::cout << "Server get: " << buff << " from " << who << std::endl;
std::string respond = func_2(buff); // 实际业务处理由上层指定
// 发送给服务器
write(sockfd_, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "Client " << who << " " << sockfd_ << " quit!" << std::endl;
close(sockfd_); // 关闭文件描述符
}
else
{
// 读取出问题(暂时)
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sockfd_); // 关闭文件描述符
}
}
private:
int sockfd_;//accept返回的套接字
std::string clientip_;//用户IP
uint16_t clientport_;//用户端口号
func_t func_2; // 业务处理函数
};
server.hpp代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include<pthread.h>
#include"Threadpool.hpp"
#include"Task.hpp"
namespace nt_server
{
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog = 5; // 请求队列的最大长度
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR
};
class TcpServer; // 前置声明
// 包含我们所需参数的类型
class ThreadData
{
public:
ThreadData(int sock, const std::string& ip, const uint16_t& port, TcpServer* ptr)
:sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
{}
// 设置为公有是为了方便访问
public:
int sock_;
std::string clientip_;
uint16_t clientport_;
TcpServer* current_; // 指向 TcpServer 对象的指针
};
using func_t = std::function<std::string(std::string)>; // 回调函数类型
class TcpServer
{
public:
TcpServer(const func_t &func, const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port), func_1(func) // 注意这里要传1个业务处理函数
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if (bind(listen_sock_, (const sockaddr *)&local, sizeof(local)) < 0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if (listen(listen_sock_, backlog) < 0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
for (;;)
{
//线程池的设计--》使用之前必须先启动
ThreadPool<Task>::GetInstance()->Start();
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int accept_socket = accept(listen_sock_, (struct sockaddr *)&client, &len);
if (accept_socket < 0)
{
std::cout << "accept failed" << std::endl;
continue;
}
// 2.业务处理
// 2.1客户端信息存储
uint16_t clientport = ntohs(client.sin_port); // 客户端端口号
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 客户端IP
std::cout << "Server accept " << clientip << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
// 3.把任务交给线程池
Task task1(accept_socket,clientip,clientport,func_1);//和原来Server函数的参数差不多,这里还多传了一个回调函数
ThreadPool<Task>::GetInstance()->Push(task1);
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
func_t func_1; // 业务处理函数
};
}
server.cc代码呈现:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
using namespace nt_server;
// 业务处理回调函数(字符串回响)
string echo(string request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<TcpServer> usvr (new TcpServer(echo,port));
usvr->InitServer();
usvr->StartServer();
return 0;
}
2.6 守护进程版服务器
2.6.1 守护进程( Daemon)
概念:
- 守护进程是一种长期运行的进程(守护进程的生存期不一定长,但一般应该这样做),一般是操作系统启动的时候它就启动,操作系统关闭的时候它才关闭。
- 守护进程跟终端无关联,也就是说它们没有控制终端,所以控制终端退出,也不会导致守护进程退出。
- 守护进程是在后台运行的,不会占着终端,终端可以执行其他命令。
分析:
- 守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生;它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。
- 守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机才随之一起停止运行。
- 守护进程一般都以root用户权限运行,因为要使用某些特殊的端口(1-1024)或者资源。
- 守护进程的父进程一般都是init进程,因为它真正的父进程在fork出守护进程后就直接退出了,所以守护进程都是孤儿进程,由init接管。
- 守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
- 守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。
细节说明:
- 守护进程是一个生存周期较长的进程,通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件。
- 大多数服务都是通过守护进程实现的。
- 关闭终端,相应的进程都会被关闭,而守护进程却能够突破这种限制。
总结:
- 守护进程不会收到来自内核的 SIGHUP 信号,也就是说,如果守护进程收到了 SIGHUP 信号,那么肯定是另外的进程发的。
- 守护进程把 SIGHUP 信号作为通知信号,表示配置文件已经发生改动,守护进程应该重新读入其配置文件。
- 守护进程不会收到来自内核的 SIGINT 信号(Ctrl+C)、SIGWINCH 信号(终端窗口大小改变)。
2.6.2 进程组
什么是进程组:
进程组就是一个或多个进程的集合。这些进程并不是孤立的,他们彼此之间或者存在父子、兄弟关系,或者在功能上有相近的联系。每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。
Linux为什么要有进程组:
提供进程组就是为了方便对进程进行管理。假设要完成一个任务,需要同时并发100个进程。当用户处于某种原因要终止 这个任务时,要是没有进程组,就需要手动的一个个去杀死这100个进程,并且必须要严格按照进程间父子兄弟关系顺序,否则会扰乱进程树。
修改进程组ID的接口:
int setpgid(pid_t pid, pid_t pgid);
这个函数的含义是,找到进程ID为pid的进程,将这个进程的进程组ID修改为pgid,如果pid的值为0,则表示要修改调用进程的进程组ID。该接口一般用来创建一个新的进程组。
2.6.3 作业
概念:
Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。
作业与进程组的区别:
- 如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。
- 一个或多个进程组的集合,比如用户从登陆到退出,这个期间用户运行的所有进程都属于该会话周期。
2.6.4 会话
概念:
由于Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。 sys_setsid()调用能创建一个会话。
语法:
#include <unistd.h>
pid_t setsid(void);
如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
- 创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
- 创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
- 该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。
2.6.5 控制终端
概念:
与控制终端建立连接的会话领头进程称为控制进程 (session leader) ,一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。
总结:
- 进程属于一个进程组,进程组属于一个会话,会话可能有也可能没有控制终端。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。
- 同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。
- 同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。
2.6.6 创建守护进程的过程
①fork()创建子进程,父进程exit()退出:
这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。
②在子进程中调用 setsid() 函数创建新的会话:
在调用了fork()函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。
③再次 fork() 一个孙进程并让子进程退出:
为什么要再次fork呢,假定有这样一种情况,之前的父进程fork出子进程以后还有别的事情要做,在做事情的过程中因为某种原因阻塞了,而此时的子进程因为某些非正常原因要退出的话,就会形成僵尸进程,所以由子进程fork出一个孙进程以后立即退出,孙进程作为守护进程会被init接管,此时无论父进程想做什么都随它了。
④在孙进程中调用 chdir() 函数,让根目录 ”/” 成为孙进程的工作目录:
这一步也是必要的步骤,使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,改变工作目录的常见函数是chdir。
⑤在孙进程中调用 umask() 函数,设置进程的文件权限掩码为0:
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。
⑥在孙进程中关闭任何不需要的文件描述符:
- 同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。
- 在上面的第2)步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。
⑦守护进程退出处理:
当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。
2.6.7 直接调用系统现成的接口
语法:
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数说明:
- nochdir:如果该参数为0,则将当前工作目录更改为根目录;如果为1,则不更改当前工作目录。
- noclose:如果该参数为0,则关闭所有与终端相关的文件描述符;如果为1,则不关闭文件描述符。
server.cc
服务器源文件:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
using namespace nt_server;
// 业务处理回调函数(字符串回响)
string echo(string request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
// 直接守护进程化
daemon(0, 0);
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<TcpServer> usvr (new TcpServer(echo,port));
usvr->InitServer();
usvr->StartServer();
return 0;
}
2.6.8 自己创建守护进程版本的服务器
手动实现守护进程时需要注意以下几点:
- 忽略异常信号
- 0、1、2 要做特殊处理(文件描述符)
- 进程的工作路径可能要改变(从用户目录中脱离至根目录)
具体实现步骤如下:
- 忽略常见的异常信号:SIGPIPE、SIGCHLD。
- 如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程。
- 新建会话,自己成为会话的 话首进程。
- (可选)更改守护进程的工作路径:chdir。
- 处理后续对于 0、1、2 的问题。
对于标准输入、标准输出、标准错误 的处理方式有两种:
- 暴力处理:直接关闭 fd
- 优雅处理:将 fd 重定向至 /dev/null,也就是 daemon() 函数的做法
2.6.9 代码呈现
Daemon.hpp
守护进程头文件:
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LIS_ERR,
CONNECT_ERR,
FORK_ERR,
SETSID_ERR,
CHDIR_ERR,
OPEN_ERR
};
//因为这个头文件会被server.hpp包含,server.hpp会被server.cc包含,
//刚好这三个文件里都要使用这些退出码信息,所以放在这里一次即可
static const char *path = "/home/zs_108/A";//设置守护进程的工作目录,这里大家要自己设置啊
void Daemon()
{
// 1、忽略常见信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2、创建子进程,自己退休
pid_t id = fork();
if (id > 0)
exit(0);
else if (id < 0)
{
// 子进程创建失败
std::cout<<"Fork Fail: "<<strerror(errno)<<std::endl;
exit(FORK_ERR);
}
// 3、新建会话,使自己成为一个单独的组
pid_t ret = setsid();
if (ret == -1)
{
// 守护化失败
std::cout<<"Setsid Fail: "<<strerror(errno)<<std::endl;
exit(SETSID_ERR);
}
// 4、更改工作路径
int n = chdir(path);
if (n == -1)
{
// 更改路径失败
std::cout<<"Chdir Fail: "<<strerror(errno)<<std::endl;
exit(CHDIR_ERR);
}
// 5、重定向标准输入输出错误
int fd = open("/dev/null", O_RDWR);
if (fd == -1)
{
// 文件打开失败
std::cout<<"Open Fail: "<<strerror(errno)<<std::endl;
exit(OPEN_ERR);
}
// 重定向标准输入、标准输出、标准错误
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
Task.hpp代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include<pthread.h>
using func_t = std::function<std::string(std::string)>; // 回调函数类型
// 任务类
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport,const func_t &func)
: sockfd_(sockfd), clientip_(clientip), clientport_(clientport), func_2(func) // 注意这里要传1个业务处理函数
{
}
~Task()
{
}
void Run()
{
char buff[1024];
std::string who = clientip_ + "-" + std::to_string(clientport_);
//while (true),如果线程池的线程一直循环在干这件事的话,效率会极其低下
ssize_t n = read((sockfd_),buff, sizeof(buff)- 1); // 预留 '\0' 的位置
if (n > 0)
{
// 读取成功
buff[n] = '\0';
std::cout << "Server get: " << buff << " from " << who << std::endl;
std::string respond = func_2(buff); // 实际业务处理由上层指定
// 发送给服务器
write(sockfd_, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "Client " << who << " " << sockfd_ << " quit!" << std::endl;
close(sockfd_); // 关闭文件描述符
}
else
{
// 读取出问题(暂时)
std::cerr << "Read Fail!" << strerror(errno) << std::endl;
close(sockfd_); // 关闭文件描述符
}
close(sockfd_);//由于没有死循环,我们必须将其关闭
}
private:
int sockfd_;//accept返回的套接字
std::string clientip_;//用户IP
uint16_t clientport_;//用户端口号
func_t func_2; // 业务处理函数
};
ThreadPool.hpp代码:
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
// 线程信息结构体
struct ThreadInfo
{
pthread_t tid; // 线程ID
std::string name; // 线程名称
};
// 默认线程数量
static const int defalutnum = 5;
// 线程池模板类
template <class T>
class ThreadPool
{
private:
// 互斥锁加锁函数
void Lock()
{
pthread_mutex_lock(&mutex_);
}
// 互斥锁解锁函数
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
// 唤醒等待的线程
void Wakeup()
{
pthread_cond_signal(&cond_);
}
// 线程休眠等待条件变量
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
// 判断任务队列是否为空
bool IsQueueEmpty()
{
return tasks_.empty();
}
// 根据线程ID获取线程名称
std::string GetThreadName(pthread_t tid)
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
// 线程处理任务的函数
static void *HandlerTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t.Run();
}
}
// 启动线程池中的所有线程
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
}
}
// 从任务队列中取出一个任务
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
// 向任务队列中添加一个任务
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
// 获取线程池单例对象
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // 如果线程池对象不存在,则创建一个新的线程池对象
{
pthread_mutex_lock(&lock_); // 加锁保证线程安全
if (nullptr == tp_) // 再次检查是否已经创建了线程池对象,防止多线程环境下的竞争条件
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>(); // 创建线程池对象
}
pthread_mutex_unlock(&lock_); // 解锁
}
return tp_; // 返回线程池对象指针
}
private:
// 构造函数,初始化线程池,可以指定线程数量,默认为defalutnum
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr); // 初始化互斥锁
pthread_cond_init(&cond_, nullptr); // 初始化条件变量
}
// 析构函数,销毁线程池资源
~ThreadPool()
{
pthread_mutex_destroy(&mutex_); // 销毁互斥锁
pthread_cond_destroy(&cond_); // 销毁条件变量
}
// 禁止拷贝构造和赋值操作符,确保线程池对象的单一性
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
// 线程信息列表
std::vector<ThreadInfo> threads_;
// 任务队列
std::queue<T> tasks_;
// 互斥锁和条件变量用于同步和通信
pthread_mutex_t mutex_;
pthread_cond_t cond_;
// 线程池单例对象指针和互斥锁静态成员变量
static ThreadPool<T> *tp_;
static pthread_mutex_t lock_;
};
// 初始化线程池单例对象指针和互斥锁静态成员变量
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
server.hpp代码:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Threadpool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int backlog = 5; // 请求队列的最大长度
class TcpServer; // 前置声明
// 包含我们所需参数的类型
class ThreadData
{
public:
ThreadData(int sock, const std::string &ip, const uint16_t &port, TcpServer *ptr)
: sock_(sock), clientip_(ip), clientport_(port), current_(ptr)
{
}
// 设置为公有是为了方便访问
public:
int sock_;
std::string clientip_;
uint16_t clientport_;
TcpServer *current_; // 指向 TcpServer 对象的指针
};
using func_t = std::function<std::string(std::string)>; // 回调函数类型
class TcpServer
{
public:
TcpServer(const func_t &func, const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port), func_1(func) // 注意这里要传1个业务处理函数
{
}
~TcpServer()
{
}
// 初始化服务器
void InitServer()
{
// 1.创建套接字
listen_sock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock_ == -1)
{
std::cerr << "Create Socket Fail!" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "Create Socket Success! " << listen_sock_ << std::endl;
// 2.绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); // 清零
local.sin_family = AF_INET; // 网络
local.sin_addr.s_addr = inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址
local.sin_port = htons(port_); // 我设置为默认是8877
if (bind(listen_sock_, (const sockaddr *)&local, sizeof(local)) < 0)
{
std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
if (listen(listen_sock_, backlog) < 0)
{
perror("Listen fail");
exit(LIS_ERR);
}
}
// 启动服务器
void StartServer()
{
// 守护进程化
Daemon();
for (;;)
{
// 线程池的设计--》使用之前必须先启动
ThreadPool<Task>::GetInstance()->Start();
// 1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int accept_socket = accept(listen_sock_, (struct sockaddr *)&client, &len);
if (accept_socket < 0)
{
std::cout << "accept failed" << std::endl;
continue;
}
// 2.业务处理
// 2.1客户端信息存储
uint16_t clientport = ntohs(client.sin_port); // 客户端端口号
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 客户端IP
std::cout << "Server accept " << clientip << " - " << clientport << " " << accept_socket << " from " << listen_sock_ << " success!" << std::endl;
// 3.把任务交给线程池
Task task1(accept_socket, clientip, clientport, func_1); // 和原来Server函数的参数差不多,这里还多传了一个回调函数
ThreadPool<Task>::GetInstance()->Push(task1);
}
}
private:
int listen_sock_; // socket套接字
uint16_t port_; // 端口号
std::string ip_; // ip地址
func_t func_1; // 业务处理函数
};
server.cc代码:
#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
using namespace std;
// 业务处理回调函数(字符串回响)
string echo(string request)
{
return request;
}
void Usage(const char* program)
{
cout << "Usage:" << endl;
cout << "\t" << program << " ServerPort" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
// 错误的启动方式,提示错误信息
Usage(argv[0]);
return USAGE_ERR;
}
//命令行参数都是字符串,我们需要将其转换成对应的类型
uint16_t port = stoi(argv[1]);//将字符串转换成端口号
unique_ptr<TcpServer> usvr (new TcpServer(echo,port));
usvr->InitServer();
usvr->StartServer();
return 0;
}
2.7 问题剖析
2.7.1实际连接过程没有这么简单
解释:
真正连接的过程实际就是双方操作系统三次握手的过程,这个过程是由双方的操作系统自动完成的。 我们知道上层发起连接请求和收获连接结果是通过connect和accept系统调用来完成的,而真实的连接过程和这两个系统调用没什么关系,连接过程是由双方的操作系统执行各自的内核代码自动完成连接过程的。
所以accept并不参与三次握手的任何细节,他仅仅只负责拿走连接结果的胜利果实。换句话说,就算上层不调用accept,三次握手的过程也能够建立好,因为应用是应用,底层是底层,三次握手就是底层,和你应用没半毛钱关系,这是我双方的操作系统自主完成的工作。
另外我们所说的TCP协议保证可靠性和应用有关系吗?照样没半毛钱关系!因为应用是应用,底层是底层,TCP协议是传输层的,传输层在操作系统内部实现。
2.7.2 维护TCP的连接有成本嘛
答案有:
一定是有的,因为双方的操作系统要在各自底层建立描述连接的结构对象,然后用数据结构将这些结构对象管理起来,这些都是要花时间和内存空间的,所以维护连接一定是有成本的。
2.7.3 简单理解三次握手和四次挥手
理解三次握手和四次挥手:
- 在链接过程中,tcp采用三次握手。
- 在断线过程中,tcp采用四次挥手。
三次握手:
client调用connect,向服务器发起连接请求,connect会发出SYN段并阻塞等待服务器应答(第一次),服务器收到客户端的SYN段后,会给客户端应答一个SYN-ACK段表示"同意建立连接"(第二次),客户端收到SYN-ACK段后会从connect系统调用返回,同时应答一个ACK段(第三次),此时连接建立成功。
四次握手:
客户端如果没有请求之后,就会调用close关闭连接,此时客户端会向服务器发送FIN段(第一次),服务器收到FIN段后,会回应一个ACK段(第二次),同时服务器的read会读到0,当read返回后服务器就会知道客户端关闭了连接,他此时也会调用close关闭连接,同时向客户端发送FIN段(第三次), 客户端收到FIN段后,会给服务器返回一个ACK段(第四次)。 (socketAPI的connect被调用时会发出SYN段,read返回时表明服务器收到FIN段)
2.7.4 TCP通信的实质
TCP通信的实质:
- 这些全部都是由TCP协议自己决定的,这是操作系统内部的事情,和我们用户层没有任何瓜葛,这也就是为什么TCP叫做传输控制协议的原因,因为传输的过程是由他自己所控制决定的。
- c->s和s->c之间发送使用的是不同对的发送和接收缓冲区,所以c给s发是不影响s给c发送的,这也就能说明TCP是全双工的,一个在发送时,不影响另一个也再发送,所以网络发送的本质就是数据拷贝。
应用层缓冲区是什么:
其实所谓的应用层缓冲区就是我们自己定义的buffer,可以看到下面的6个网络发送接收接口都有对应的buf形参,我们在使用的时候肯定要传参数进去,而传的参数就是我们在应用层所定义出来的缓冲区。
2.8 封装接口
Socket.hpp代码:
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定义一些错误代码
enum
{
SocketErr = 2, // 套接字创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列的长度
const int backlog = 10;
class Sock //服务器专门使用
{
public:
Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字
{
}
~Sock()
{
// 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源
}
// 创建套接字
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
printf("socket error, %s: %d", strerror(errno), errno); //错误
exit(SocketErr); // 发生错误时退出程序
}
int opt=1;
setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //关闭后快速重启
}
// 将套接字绑定到指定的端口上
void Bind(uint16_t port)
{
//让服务器绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));//清零
local.sin_family = AF_INET; // 网络
local.sin_port = htons(port); // 我设置为默认绑定任意可用IP地址
local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) //让自己绑定别人
{
printf("bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 监听端口上的连接请求
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
printf("listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受一个连接请求
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
printf("accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd; // 返回新的套接字文件描述符
}
// 连接到指定的IP和端口——客户端才会用的
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;//服务器的信息
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
// 关闭套接字
void Close()
{
close(sockfd_);
}
// 获取套接字的文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 套接字文件描述符
};
server.hpp代码:
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
class TcpServer
{
public:
TcpServer(const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~TcpServer()
{
}
bool InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
void Start()
{
while(true)
{
std::string clientip;
uint16_t clientport;
int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字
if(socket<0)
continue;
//提供服务
if(fork()==0)
{
listensock_.Close();
//通过sockfd使用提供服务
std::string inbuf;
while (1)
{
char buf[1024];
// 1.读取客户端发送的信息
ssize_t s = read(sockfd, buf, sizeof(buf) - 1);
if (s == 0)
{ // s == 0代表对方发送了空消息,视作客户端主动退出
printf("client quit: %s[%d]", clientip.c_str(), clientport);
break;
}
else if (s < 0)
{
// 出现了读取错误,打印错误后断开连接
printf("read err: %s[%d] = %s", clientip.c_str(), clientport, strerror(errno));
break;
}
else // 2.读取成功
{
}
}
exit(0);//子进程退出
}
close(sockfd);//
}
}
private:
uint16_t port_;
Sock listensock_;//专门用来listen的
std::string ip_; // ip地址
}
三、结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。
更多推荐
所有评论(0)