目录
一、TCP通信协议的封装
二、TCP多进程通信
三、TCP多线程通信
简单的TCP一对一通信其实完全可以不进行封装,直接分别写server端和client端的源代码,按照TCP通信协议的规定调用socket接口即可完成,但是在通过TCP协议设计应用层协议的时候,将TCP协议进行封装并且将协议与服务任务进行解耦,是更方便编程和维护的。
对server服务端封装,我们只需要指明服务器的端口号,服务器IP是本机IP,服务器可以接收的IP是任何主机的IP。
对client客户端封装,我们需要指明服务器的ip和端口号,通过服务器的ip和端口号信息,构建sockaddr_in结构体,然后通过sockaddr_in结构体对服务器发起连接请求。
server服务器的类分为两个阶段:
client客户端也分为两个阶段:
在网络通信中,需要调用socket套接字的接口完成,这些接口的头文件是:
#include
#include
#include
#include
Server.h头文件:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const string g_default_ip = "0.0.0.0";
class TcpServer
{
public:
TcpServer(const uint16_t port, const string& ip = g_default_ip)
: _port(port), _ip(ip), _listenfd(0)
{}
void Init()
{
// 1. 创建socket套接字
_listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listenfd < 0)
{
cerr << "socket error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 2. bind绑定服务器网络信息
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(_port);
if (bind(_listenfd, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 3. listen设置监听
if (listen(_listenfd, 8) < 0)
{
// 监听的连接队列长度与项目的线程数相关
cerr << "listen error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
}
void Start()
{
// 4. accept连接客户端
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int client_sock = accept(_listenfd, (struct sockaddr*)&client, &client_len);
if (client_sock < 0)
{
cerr << "accept error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 5. 连接成功,进行通信
string client_ip = inet_ntoa(client.sin_addr);
uint16_t client_port = ntohs(client.sin_port);
while (true)
{
// 5.1 接收信息
char recv_buf[1024];
int n = read(client_sock, recv_buf, sizeof(recv_buf));
if (n > 0)
recv_buf[n] = 0;
cout << "[" << client_ip << ":" << client_port << "]# " << recv_buf << endl;
// 5.2 应答信息
char sent_buf[1024];
snprintf(sent_buf, sizeof(sent_buf), "服务器已收到信息: %s\n", recv_buf);
write(client_sock, sent_buf, sizeof(sent_buf));
}
}
private:
string _ip;
uint16_t _port;
int _listenfd;
};
server.cpp源文件
#include "Server.h"
#include
void Usage()
{
cout << "Usage:\n\tserver port" << endl;
exit(1);
}
int main(int args, char* argv[])
{
if (args != 2)
Usage();
uint16_t port = atoi(argv[1]);
unique_ptr tcp_server(new TcpServer(port));
tcp_server->Init();
tcp_server->Start();
return 0;
}
Client.h头文件:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class TcpClient
{
public:
TcpClient(const uint16_t server_port, const string server_ip)
: _server_port(server_port), _server_ip(server_ip), _sock(-1)
{}
void Init()
{
// 1. 创建套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
cerr << "socket error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 2. bind绑定,由OS绑定
}
void Run()
{
// 3. 向服务器发起连接请求
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_server_ip.c_str());
server.sin_port = htons(_server_port);
if (connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
cerr << "connect error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 4. 连接成功,进行通信
while (true)
{
// 4.1 发送信息
char sent_buf[1024];
cout << "请输入信息:";
gets(sent_buf);
write(_sock, sent_buf, sizeof(sent_buf));
// 4.2 接收应答信息
char recv_buf[1024];
int n = read(_sock, recv_buf, sizeof(recv_buf));
if (n > 0)
recv_buf[n] = 0;
cout << recv_buf << endl;
}
}
private:
string _server_ip;
uint16_t _server_port;
int _sock;
};
client.cpp源文件:
#include "Client.h"
#include
void Usage()
{
cout << "Usage:\n\tclient ip port" << endl;
exit(1);
}
int main(int args, char* argv[])
{
if (args != 3)
Usage();
string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
unique_ptr tcp_client(new TcpClient(server_port, server_ip));
tcp_client->Init();
tcp_client->Run();
return 0;
}
上面对于TCP的封装是实现了一对一的服务器与客户端通信,通信断开代码就运行结束。
但是在业务中,一个服务器通常是需要与多个客户端通信的,并且客户端断开通信时并不会影响服务端的运行。
为了实现一对多的服务,服务器通常会采用多进程或者多线程策略。
多进程相比于多线程,资源开销更大,但是更安全,因为进程之间是完全相互独立的。
要将上面的TCP封装修改成多进程版,我们只需要将Server.h中封装TcpServer类的Start()函数做出修改即可,将其改为可以循环accept接受不同的client连接,每新建一个连接,就创建一个子进程去完成通信任务,然后主进程继续accept等待之后的client连接。
为了使多进程接受连接与任务代码解耦,我们在TcpServer类中新建一个Task()函数。
void Start()
{
while (true)
{
// 4. accept连接客户端
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int client_sock = accept(_listenfd, (struct sockaddr*)&client, &client_len);
if (client_sock < 0)
{
cerr << "accept error " << errno << ": " << strerror(errno) << endl;
exit(1);
}
// 5. 连接成功,进行通信, 多进程
string client_ip = inet_ntoa(client.sin_addr);
uint16_t client_port = ntohs(client.sin_port);
if (fork() == 0)
{
// 子进程执行通信任务
Task(client_sock, client_ip, client_port);
}
}
}
void Task(int client_sock, string client_ip, uint16_t client_port)
{
while (true)
{
// 5.1 接收信息
char recv_buf[1024];
int n = read(client_sock, recv_buf, sizeof(recv_buf));
if (n > 0)
recv_buf[n] = 0;
cout << "[" << client_ip << ":" << client_port << "]# " << recv_buf << endl;
// 5.2 应答信息
char sent_buf[1024];
snprintf(sent_buf, sizeof(sent_buf), "服务器已收到信息: %s\n", recv_buf);
write(client_sock, sent_buf, sizeof(sent_buf));
}
}
TCP通信的多线程和多进程非常相似,在相同的代码处做出修改即可,相比于多进程,多线程的对服务器来说资源开销更小,故在TCP通信中,更推荐使用多线程。
多线程策略和多进程策略总体类似,但是在代码还是存在差异,需要将Task()设置为静态函数,这其实也就是说,需要将Task()函数与TcpServer类再次解耦。当任务非常复杂的时候,我们甚至需要单独写一个Task头文件写通信任务。
在子线程执行与客户端的通信服务的时候,需要将子线程进行detach()线程分离,这样当客户端与服务器通信结束的时候,不会影响主线程,但是需要注意的事,在任务函数中当任务结束的时候,不能再用exit()退出,而是需要将exit()改为return,不然的话exit()会在退出任务的时候将退出信号传递给OS,OS会杀死整个进程。
还有就是此处我们调用C++11的线程库,不仅需要包含
makefile:
all: server client
server: server.cc
g++ -o $@ $^ -std=c++11 -pthread
client: client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf server client
Server.h代码修改处:
static void Task(int client_sock, string client_ip, uint16_t client_port)
{
while (true)
{
// 5.1 接收信息
char recv_buf[1024];
int n = read(client_sock, recv_buf, sizeof(recv_buf));
if (n == 0)
return;
recv_buf[n] = 0;
cout << "[" << client_ip << ":" << client_port << "]# " << recv_buf << endl;
// 5.2 应答信息
char sent_buf[1024];
snprintf(sent_buf, sizeof(sent_buf), "服务器已收到信息: %s\n", recv_buf);
write(client_sock, sent_buf, sizeof(sent_buf));
}
}
void Start()
{
while (true)
{
// 4. accept连接客户端
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int client_sock = accept(_listenfd, (struct sockaddr*)&client, &client_len);
if (client_sock < 0)
{
Log_Message(FATAL, "accept error");
exit(ACCEPT_ERR);
}
Log_Message(NORMAL, "accept success");
// 5. 连接成功,进行通信, 多线程
string client_ip = inet_ntoa(client.sin_addr);
uint16_t client_port = ntohs(client.sin_port);
thread t(Task, client_sock, client_ip, client_port); // 创建线程自动执行
t.detach(); // 线程分离
}
}