【Linux网络编程】套接字使用--TCP echo server的实现

目录

前言

服务器实现

初始化TCPserver

接收客户端的连接请求

提供echo服务

服务器调用逻辑

客户端实现


前言

《套接字接口》一文中,我们介绍了TCP通信时的接口

这一期我们将基于接口的说明,自己手动编写一个基于TCP的echo服务


什么是TCP Echo Server?

TCP echo server 是一种网络服务,它接受来自客户端的 TCP 连接,并将收到的每一个数据包原封不动地返回给客户端。

简单来说就是我们需要实现服务端与客户端

客户端向服务器发送一个字符串数据,而服务器需要把客户端发送的数据重新发送回给客户端

对于Echo服务的功能,其实并没有什么太大的意义,重要的是我们要学习如何基于TCP协议进行跨主机通信!

并且这一篇文章为了简单易懂,尽量会减少对socket接口的封装


服务器实现

要写出一个echo服务,首先需要经历3个步骤

  • 初始化TCP server
  • 接收客户端的连接请求
  • 提供echo服务
cpp 复制代码
class TCPServer
{
    TCPServer(uint16_t port)//通过端口号构造一个TCPServer对象
    :_port(port) , _isrunning(false)
    {}
    void InitServer(); //初始化TCPServer
    void Accept();//接收客户端的请求
    void Loop();//提供echo服务

    protected:
        uint16_t _port; //端口号
        int _listensock; 
        bool _isrunning; //控制Server是否运行
}

初始化TCPserver

需要初始化一个TCPServer对象,同样也要经历三步

  • 创建套接字
  • 绑定套接字
  • 设置套接字为listen状态
cpp 复制代码
    void InitServer() //初始化TCPServer
    {
        //1、创建套接字
        CreateSocket();
        //2、绑定套接字
        BindSocket();
        //3、设置套接字为listen状态
        SetListenSocket();
    }

创建套接字

TCP创建套接字使用的接口是socket

调用socket()系统调用创建一个套接字,指定通信协议(TCP/UDP)、地址族(IPv4/IPv6)和套接字类型。

cpp 复制代码
int socket(int domain, int type, int protocol);
  • domain:指定通信域,常见值有AF_INET(IPv4)和AF_INET6(IPv6)。
  • type:指定套接字类型,常见类型为SOCK_STREAM(TCP)和SOCK_DGRAM(UDP)。
  • protocol指定具体的协议,通常为0(系统自动选择合适的协议)。
  • 返回值:若成功,一个文件描述符(套接字)被返回。若失败,-1被返回,错误码被设置。
  • 头文件:sys/types.h 和 sys/socket.h
cpp 复制代码
    void CreateSocket()
    {
        _listensock = socket(AF_INET,SOCK_STREAM,0);
        if(_listensock < 0)
        {
            std::cerr << "create socket error!\n" << std::endl;
            exit(1);
        }
    }

绑定套接字

TCP绑定套接字使用的系统调用是bind

bind 系统调用用于将套接字(socket)绑定到一个特定的地址和端口号上。这个调用主要用于服务器端,目的是将一个特定的网络地址与一个套接字关联,使得操作系统能够将收到的数据包交给这个套接字处理。

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:套接字描述符,通常是通过 socket() 系统调用创建的套接字的文件描述符。
  • addr:指向 sockaddr 结构体的指针。这个结构体包含了要绑定的地址信息。实际使用中,这通常是一个 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)结构体的指针。
  • addrlen:表示addr结构体的大小,以字节为单位。通常使用 sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6)
  • 返回值:若成功,返回0。若失败,返回-1,且错误码被设置
  • 头文件:sys/types.h 和 sys/socket.h

补充:sockaddr和sockaddr_in

sockaddr:是一个通用的地址结构体,用于定义套接字的地址。它是一个通用的数据结构,适用于各种协议族(如 IPv4、IPv6)。但由于它的通用性,它没有具体的字段用于存储 IP 地址或端口号,需要通过特定的派生结构(如 sockaddr_in)来实现这些功能。

sockaddr_in:sockaddr_in是sockaddr的专用版本,专门用于 IPv4 地址。这个结构体非常常用,因为它能够明确存储 IP 地址和端口号,适合用于 IPv4 网络通信。

总的来说,我们基于IPv4进行通信时,一般不会定义一个sockaddr类型,一般定义一个sockaddr_in,把定义出来的sockaddr_in对象填充完毕以后,强转为sockaddr类型后传参

sockaddr_in的头文件: netinet/in.h 和 arpa/inet.h

sockaddr_in要填充的字段:

  • sin_addr.s_addr:这个填写的是IP地址的网络字节序,但服务器一般不会固定一个IP。所以这个参数填写INADDR_ANY即可。这意味着服务器将接收发往机器上任何网络接口的流量,而不仅仅是特定的 IP 地址。
  • sin_family:IPv4下一般固定为AF_INET
  • sin_port:表示该服务所绑定的端口号的网络字节序,主机序转为网络字节序的接口是htons

绑定套接字的实现

cpp 复制代码
    void BindSocket()
    {
        sockaddr_in addr;
        addr.sin_addr.s_addr = INADDR_ANY;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);
        int n = bind(_listensock,reinterpret_cast<sockaddr*>(&addr),sizeof(addr));
        if(n < 0)
        {
            std::cerr << "Server Bind Error!" << std::endl;
            exit(2);
        }
    }

设置套接字为listen状态

对于TCP来说,要实现两台主机通信的前提是两台主机必须建立连接。而服务器要从绑定好的套接字中获取连接,那么需要设置该套接字为listen状态

设置套接字为listen状态的接口是listen

cpp 复制代码
int listen(int sockfd, int backlog);
  • sockfd:这是一个已通过socket系统调用创建的套接字描述符。这个套接字必须是一个 TCP 套接字(即,SOCK_STREAM类型),才能使用listen
  • backlog:这个参数与TCP底层的全连接队列有关,之后会出一篇文章详细说明。

backlog的值该设置为多少?

  • 一般来说,常见的 backlog 值范围在 5 到 1024 之间。具体的推荐值取决于你的应用场景:

  • 对于小型应用或测试环境,backlog 值可以设置为 5 到 20。

  • 对于中等负载的生产环境,可能需要设置为 50 到 100。

  • 对于高负载、大规模的生产环境,可能需要设置为 500 或更高。

cpp 复制代码
    void SetListenSocket()
    {
        int n = listen(_listensock,8);
        if(n < 0)
        {
            std::cerr << "Set Listen Status Error!" << std::endl;
            exit(2);
        }
    }

接收客户端的连接请求

当我们完成初始化工作以后,这个TCP服务器已经可以接收客户端的连接请求了,但仅仅如此还不够。服务器在为客户端提供服务之前,需要先同意客户端的连接请求


接受客户端的连接

接受客户端的连接,我们使用的系统调用是accept

accept()系统调用的作用是从一个已绑定的、监听状态的套接字中接受一个新的连接。这个套接字通常是由socket()创建并通过bind()和listen进行设置的。调用后,系统会从等待队列中取出一个连接请求,并返回一个新的套接字,这个套接字用于与客户端进行通信。

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: 监听套接字的文件描述符,即通过socket()创建并设置为监听状态的套接字。
  • addr: 指向一个sockaddr结构体的指针,用于存放客户端的地址信息。如果不需要客户端的地址信息,可以传递NULL。
  • addrlen: 指向一个socklen_t类型的变量的指针,用于传出客户端地址的长度。初始值应为addr所指向的结构体大小,调用成功后,这个变量将被设置为实际地址长度。如果不需要,可以传递NULL。
  • 返回值:若成功,返回一个新的套接字文件描述符,专门用于与客户端进行通信(区别listen套接字,listen套接字是用于获取连接的)。若失败,返回-1,错误码被设置
  • 头文件:sys/types.h 和 sys/socket.h

注意:若服务器未收到任何连接请求,那么调用accept的执行流会阻塞等待,直到有连接到来

cpp 复制代码
    void Accept(int& sockfd)//服务器提供服务之前
    {
        //1、接受客户端的连接请求
        sockfd = accept(_listensock,nullptr,nullptr);
        if(sockfd < 0)
        {
            std::cerr << "accept client link error!" << std::endl;
            exit(4);
        }
    }

提供echo服务

提供服务之前,服务器得先获取客户端的输入,也就是要知道从哪个套接字中获取。对于这一点,在accept时返回的sockfd解决了这个问题!

并且,服务器的主执行流需要不断获取连接

提供echo服务,首先要完成n步:

  • 主执行流获取连接(循环)
  • 处理TCP通信时的执行流问题
  • 提供echo服务

TCP服务的执行流问题

若服务器的主执行流去为客户端提供服务的话,由于主执行流只有一个,那么会导致一个服务器只能服务一个客户端。 而在实际当中不可能会出现一个服务器专门服务一个客户端的情况。所以一般来说TCP通信通常采用主执行流获取连接,当连接到来的时候,主执行流创建新执行流为客户端服务。

创建多执行流的一般方式有2个:

  • 多线程
  • 多进程

对于服务时间较短,一下子就好的服务,也可以采用线程池和进程池。


多进程等待问题

对于多进程来说,父进程需要完成自动等待所有的子进程。等待方式有两种

  • Linux下忽略信号SIGCHLD,即可实现自动等待子进程
  • 利用孤儿进程会被操作系统自动回收的性质,创建一个孤儿线程

多进程TCP通信代码实现

注意:在多进程提供服务之前,建议关闭对于各自进程无关的文件描述符,否则会造成文件描述符不足的问题!

  • 对于父进程来说,父进程只完成获取连接的工作,所以可以关闭accept获取的那个文件描述符
  • 对于子进程来说,子进程完成提供服务,不需要获取连接,关闭listensock文件描述符

注意:在提供服务时,recv和send都需要指定一个套接字,所以我们需要传入sockfd作为参数

cpp 复制代码
    void Loop() // 提供echo服务
    {
        // 服务器需要不断获取连接
        _isrunning = true;
        while (_isrunning)
        {
            int sockfd = -1;
            Accept(sockfd);

            // 走到这,说明客户端连接成功,通信之前需要解决执行流问题,我们先采用多进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子线程

                // 创建孤儿进程
                if (fork() > 0)
                    exit(0);
                // 关闭listensock
                close(_listensock);
                // 提供服务
                Service(sockfd);
            }
            close(sockfd);
        }
        _isrunning = false;
    }

多线程问题

多线程等待,直接由主线程或者新线程调用线程分离即可,我采用主线程调用线程分离的方式

多线程需要传递两个参数

  • 由于线程方法固定参数为void*(void*),不能调用默认成员函数,因为它带有this指针。所以我们需要设置一个静态成员方法,并且把this指针和sockfd都传入进去,可以把this指针和sockfd封装成一个ThreadData对象

多线程TCP通信代码

传入ThreadData对象后,就可以直接调用Service提供服务

cpp 复制代码
    class TCPServer;
    struct ThreadData
    {
        ThreadData(TCPServer* self, int sockfd) : _self(self) , _sockfd(sockfd)
        {}


        TCPServer* _self;
        int _sockfd;
    };
    //...

    static void* threadroutine(void* arg)
    {
        //线程分离
        pthread_detach(pthread_self());
        ThreadData* self = reinterpret_cast<ThreadData*>(arg);
        //提供服务
        self->_self-> Service(self->_sockfd);
        delete self;
    }
    void Loop() // 提供echo服务
    {
        // 服务器需要不断获取连接
        _isrunning = true;
        while (_isrunning)
        {
            int sockfd = -1;
            Accept(sockfd);
            pthread_t tid;
            ThreadData* data = new ThreadData(this,sockfd);
            pthread_create(&tid , nullptr , threadroutine , data);
        }
        _isrunning = false;
    }

服务接口Service

Serveice主要完成两步

  • 获取客户端的输入
  • 发送客户端的输入
cpp 复制代码
    void Service(int sockfd)
    {
        while (true)
        {
            //获取客户端的输入
            char buffer[1024];
            int n = recv(sockfd, buffer, sizeof(buffer), MSG_WAITALL);
            if (n > 0)
            {
                // 接收成功
                std::cout << "Client say# " << buffer << std::endl;
                // 服务器把buffer中的数据重新发送给客户端,完成服务

                //发送客户端的输入
                send(sockfd, buffer, sizeof(buffer), 0);
            }
            else if (n == 0)
            {
                // 客户端套接字关闭
                std::cout << "client socket close!" << std::endl;
                close(sockfd);
                exit(0);
            }
            else
            {
                // recv error
                std::cerr << "recv error!" << std::endl;
                close(sockfd);
                exit(5);
            }
        }
    }

服务器调用逻辑

cpp 复制代码
#include "TCPServer.h"

// ./Tcpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cout << argv[0] << " port" << std::endl;
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    TCPServer t(port);
    t.InitServer();
    t.Loop();
    return 0;
}

客户端实现

注意:对于客户端的实现不再使用任何封装

客户端需要获取两个信息

  • 服务器的端口号
  • 服务器的IP

这两个信息由用户输入


获取了端口号和IP后,客户端同样需要使用socket调用创建套接字,但无需绑定,因为在创建套接字时底层操作系统自动绑定了端口号和IP地址

创建好套接字后,客户端需要向服务器发送连接请求

发送连接请求使用的系统调用是connect

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:套接字文件描述符,是在前一步时socket的返回值
  • addr:指向一个sockaddr结构体的指针,这个结构体包含了服务器的地址信息,如 IP 地址和端口号。根据协议不同,具体的sockaddr结构体可能有不同的类型,例如sockaddr_in(IPv4)。(要填充好这个参数中的字段)
  • addrlen:addr类型的长度,IPv4下是sizeof(sockaddr_in)
  • 返回值:若成功,返回0。若失败,返回-1,错误码被设置。
  • 头文件:sys/socket.h 和 sys/types.h

客户端发起连接后,即可通过recv和send系统调用与服务器进行通信


代码实现

cpp 复制代码
#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(char *arg)
{
    std::cout << "Usage:" << arg << " ip port" << std::endl;
}

// ./tcpclient ip port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    // 获取服务器端口号和IP地址
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "client socket create error!" << std::endl;
        exit(1);
    }
    // 填充connect的sockaddr参数
    sockaddr_in peer;
    inet_pton(AF_INET, ip.c_str(), &peer.sin_addr.s_addr);
    peer.sin_family = AF_INET;
    peer.sin_port = htons(port);
    // 客户端发起连接
    int n = connect(sockfd, reinterpret_cast<sockaddr *>(&peer), sizeof(peer));
    if (n == 0)
    {
        // 客户端与服务器进行通信
        while (true)
        {
            std::cout << "Please Enter# ";
            char buffer[1024];
            std::cin >> buffer;
            send(sockfd, buffer, sizeof(buffer), 0);
            char outbuffer[1024];
            ssize_t n = recv(sockfd, outbuffer, sizeof(outbuffer), 0);
            std::cout << outbuffer << std::endl;
        }
    }
    return 0;
}

至此,最基本的一个基于TCP通信的代码已经完成,接下来看看结果展示

代码链接

相关推荐
EasyCVR11 分钟前
私有化部署视频平台EasyCVR宇视设备视频平台如何构建视频联网平台及升级视频转码业务?
大数据·网络·音视频·h.265
运维&陈同学25 分钟前
【zookeeper03】消息队列与微服务之zookeeper集群部署
linux·微服务·zookeeper·云原生·消息队列·云计算·java-zookeeper
hgdlip37 分钟前
主IP地址与从IP地址:深入解析与应用探讨
网络·网络协议·tcp/ip
珹洺1 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
今天我刷leetcode了吗1 小时前
docker 配置同宿主机共同网段的IP 同时通过通网段的另一个电脑实现远程连接docker
tcp/ip·docker·电脑
科技象限1 小时前
电脑禁用U盘的四种简单方法(电脑怎么阻止u盘使用)
大数据·网络·电脑
东方隐侠安全团队-千里1 小时前
网安瞭望台第3期:俄黑客 TAG - 110组织与密码攻击手段分享
网络·chrome·web安全·网络安全
周末不下雨1 小时前
win11+ubuntu22.04双系统 | 联想 24 y7000p | ubuntu 22.04 | 把ubuntu系统装到1T的移动固态硬盘上!!!
linux·运维·ubuntu
云计算DevOps-韩老师1 小时前
【网络云计算】2024第47周-每日【2024/11/21】周考-实操题-RAID6实操解析2
网络·云计算