【Linux后端服务器开发】TCP通信设计

目录

一、TCP通信协议的封装

二、TCP多进程通信

三、TCP多线程通信


一、TCP通信协议的封装

简单的TCP一对一通信其实完全可以不进行封装,直接分别写server端和client端的源代码,按照TCP通信协议的规定调用socket接口即可完成,但是在通过TCP协议设计应用层协议的时候,将TCP协议进行封装并且将协议与服务任务进行解耦,是更方便编程和维护的。

对server服务端封装,我们只需要指明服务器的端口号,服务器IP是本机IP,服务器可以接收的IP是任何主机的IP。

对client客户端封装,我们需要指明服务器的ip和端口号,通过服务器的ip和端口号信息,构建sockaddr_in结构体,然后通过sockaddr_in结构体对服务器发起连接请求。

server服务器的类分为两个阶段:

  • 第一个阶段是Init()初始化阶段,创建socket套接字、bind绑定本地网络信息、listen设置监听
  • 第二阶段是Start()进行服务阶段,accept连接客户端、服务任务,在本次封装中,服务任务是简单一对一通信,故我直接将任务代码写在了Start()函数里面,在任务复杂的时候,我们可以再写一个Task()任务阶段,将执行服务和服务任务的具体内容进行解耦

client客户端也分为两个阶段:

  • 第一个阶段是Init()初始化阶段,创建socket套接字,不需要调用bind()函数,由os绑定
  • 第二个阶段是Run()运行阶段,通过服务器的sockaddr_in信息向服务器发起connect请求,连接成功后即可直接进行通信

在网络通信中,需要调用socket套接字的接口完成,这些接口的头文件是:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

Server.h头文件:

#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <functional>
#include <cerrno>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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 <memory>

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<TcpServer> tcp_server(new TcpServer(port));

    tcp_server->Init();
    tcp_server->Start();

    return 0;
}

Client.h头文件:

#pragma once

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <functional>
#include <cerrno>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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 <memory>

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<TcpClient> tcp_client(new TcpClient(server_port, server_ip));

    tcp_client->Init();
    tcp_client->Run();

    return 0;
}

二、TCP多进程通信

上面对于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通信的多线程和多进程非常相似,在相同的代码处做出修改即可,相比于多进程,多线程的对服务器来说资源开销更小,故在TCP通信中,更推荐使用多线程。

多线程策略和多进程策略总体类似,但是在代码还是存在差异,需要将Task()设置为静态函数,这其实也就是说,需要将Task()函数与TcpServer类再次解耦。当任务非常复杂的时候,我们甚至需要单独写一个Task头文件写通信任务。

在子线程执行与客户端的通信服务的时候,需要将子线程进行detach()线程分离,这样当客户端与服务器通信结束的时候,不会影响主线程,但是需要注意的事,在任务函数中当任务结束的时候,不能再用exit()退出,而是需要将exit()改为return,不然的话exit()会在退出任务的时候将退出信号传递给OS,OS会杀死整个进程。

还有就是此处我们调用C++11的线程库,不仅需要包含<thread>头文件,在编译的时候也要加上线程动态库的选项:-lpthread

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();                                             // 线程分离
        }
    }

  • 9
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AllinTome

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值