初识Linux · TCP基本使用 · 回显服务器

news2025/5/13 21:20:34

目录

前言:

回显服务器

TCPserver_v0

TCPserver_v1--多进程版本

TCPserver_v2--多线程版本


前言:

前文我们介绍了UDP的基本使用,本文我们介绍TCP的基本使用,不过TCP的使用我们这里先做一个预热,即只是使用TCP的API简单实现一个回显服务器就可以了。在本文实现回显服务器的时候,分为了三个版本,我们从第一个不靠谱版本逐渐优化~

那么话不多说,我们直接进入回显服务器的实现。


回显服务器

对于回显服务器来说,基本功能就是客户端发送字符串,然后服务器收到这个字符串之后再给客户端发送回去,这是它的一个基本功能,那么我们从TCPserver_v1开始实现。

TCPserver_v0

对于版本一,它的弊端是只能通信一个客户端,多了会阻塞,先埋下伏笔,我们后面慢慢解释。

首先,不管是TCP还是UDP,都是基于网络套接字进行通信的,那么也就是说,TCP也需要创建套接字,bind,到了bind之后UDP就可以通信了,但是TCP因为是面向连接的,所以TCP需要额外的进入listen状态,并且开始accept,客户端要进行通信也需要connect。

具体什么是listen,什么是accept,什么是connect,我们这里展开。

对于listen来说,让服务器进入listen状态,就相当于告诉别人,我准备好了,可以开始准备连接了,使用到的API是listen:

它的参数是sockfd和backlog,对于sockfd是我们创建的套接字,这个套接字我们最好命名为listensockfd,因为这个套接字只是用来进行连接的,后面实际上服务器和客户端进行通信是通过另一个套接字进行的。对于backlog我们后面单独写一篇文章介绍。

它的返回值和socket bind是一样的,如果listen失败,返回的就是-1,成功返回的就是0,我们也可以通过netstat -ntpl,其中的l代表的就是查询处于listen状态的网络。

到了这一步,socket bind listen,我们的服务器的初始化完成了,对应的代码应该是:

    void Init()
    {
        // socket
        _listensocket = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensocket < 0)
        {
            perror("socket");
            exit(SOCKET_ERR);
        }
        // bind
        sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        socklen_t len = sizeof(server);
        if (::bind(_listensocket, (struct sockaddr *)&server, len) < 0)
        {
            perror("bind");
            exit(BIND_ERR);
        }

        // listen
        int n = ::listen(_listensocket, gbacklog);
        if (n < 0)
        {
            perror("listen");
            exit(LISTEN_ERR);
        }
    }

如果我们想像存在一个餐馆,那么listen就代表餐馆开业了,要开始服务了,要开始拉客了。

好了 现在对于tcp通信服务最难的一个点就来了,accept的理解

对于accept的参数,第一个参数是sockfd,后面的两个参数是客户端的相关信息,当我们看到返回值的时候,我们发现,如果accept成功的时候,返回一个文件描述符,错误的时候返回-1并且错误码被设置。

这里的难点是:如何理解socket返回的sockfd和accept返回的sockfd?

对于socket返回的sockfd,我们把它是作为listen的参数使用的,意在告诉别人我这个服务器已经就绪了,可以开始连接了,那么socket返回的sockfd就像是餐馆本体,进行外部的连接,对于accept返回的sockfd,就像是餐馆和客人进行了连接之后,该通过哪个服务员进行和客人的交互,所以实际上和客人进行通信的是accept返回的套接字。后面为了区分,我们将socket返回的套接字叫做监听套接字,accept返回的套接字就叫做连接套接字

有了这个套接字,我们才能和客户端进行通信,那么服务器下一步就是通过sockfd处理和客人的请求,那么因为是回显功能,在UDP的时候,使用的是sendto和recvfrom,在TCP这里就比较特殊了,因为TCP是面向连接的,那么双方经过了三次握手,获取到了对应的sockfd,不要忘了sockfd本质上是文件描述符,所以有了文件描述符,双方是可以直接使用read write进行文件读写的。

对于write的本质,是OS将用户提供的数据,拷贝到内核中的发送缓冲区,然后进行分段,加TCP报头,通过网卡发送出去,对于read的本质也是一样的,数据先到接收缓冲区,TCP负责组包一类的工作。

所以我们的service函数就可以这么写了:

    void Service(int sockfd)
    {
        while (true)
        {
            // read write
            char buffer[1024];
            ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string echo = "[Server say]# ";
                echo += buffer;
                ssize_t wn = ::write(sockfd, echo.c_str(), echo.size());
            }
            else if (n == 0)
            {
                std::cout << "client quit" << std::endl;
                break;
            }
            else
            {
                if (errno == EINTR) continue;
                std::cout << "read error" << std::endl;
                break;
            }
        }
        ::close(sockfd);
    }

直接调用,就和我们之前C语言学习的文件操作一样,并且这里有一个非常重要的点是service结束之后我们一定要close(sockfd),不然是会导致文件描述符泄露的,这个操作是非常危险的!!!

那么对于read和write的返回值,具体的我们到后面再谈。

所以这个时候,我们的TCP服务类也差不多了,对于循环服务的代码,第一个版本是这样的:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    ACCEPT_ERR,
    LISTEN_ERR
};

const static int gbacklog = 8;

class TcpServer
{
public:
    TcpServer(int port)
        : _port(port)
    {
    }

    void Init()
    {
        // socket
        _listensocket = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensocket < 0)
        {
            perror("socket");
            exit(SOCKET_ERR);
        }
        // bind
        sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = INADDR_ANY;

        socklen_t len = sizeof(server);
        if (::bind(_listensocket, (struct sockaddr *)&server, len) < 0)
        {
            perror("bind");
            exit(BIND_ERR);
        }

        // listen
        int n = ::listen(_listensocket, gbacklog);
        if (n < 0)
        {
            perror("listen");
            exit(LISTEN_ERR);
        }
    }

    void Loop()
    {
        // signal(SIGCHLD, SIG_IGN);
        bool _isrunning = true;
        while (_isrunning)
        {

            // accept
            sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                std::cout << "accept error" << std::endl;
                continue;
            }
            // service

            // version--1
            Service(sockfd);
        }
    }

    void Service(int sockfd)
    {
        while (true)
        {
            // read write
            char buffer[1024];
            ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string echo = "[Server say]# ";
                echo += buffer;
                ssize_t wn = ::write(sockfd, echo.c_str(), echo.size());
            }
            else if (n == 0)
            {
                std::cout << "client quit" << std::endl;
                break;
            }
            else
            {
                if (errno == EINTR) continue;
                std::cout << "read error" << std::endl;
                break;
            }
        }
        ::close(sockfd); // 
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensocket;
};

对于服务器的Main方法和之前UDP的时候是一样的,利用命令行参数列表给到对应的IP地址和端口号即可:

#include "TcpServer.hpp"
#include <functional>
#include <memory>


int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        perror("parameter error");
        exit(-1);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tsver = std::make_unique<TcpServer>(port);
    tsver->Init();
    tsver->Loop();

    return 0;
}

这是服务器的,对于客户端倒是有点不同了。

客户端代码编写:

客户端大部分代码和UDP那里很像,同样要定义服务器的sockaddr_in,并且填充对应的信息,这里也有一个亘古不变的话题,客户端是否需要显示的bind自己的sockfd和sockaddr_in?

当然是不需要的,因为这个操作OS已经隐式的帮我们做了,我们不用自己做了。

所以当我们创建好了server的sockaddr_in 并且相关的字段也填充好了,然后就是TCP专有的操作了,客户端需要使用connect进行连接,使用connect发起三次握手,当然具体操作我们后面介绍,我们现在只需要知道客户端需要进行connect和服务器进行连接就行,

它的参数就是客户端的sockfd,后面的两个参数是和谁连接的sockaddr_in。

连接好了之后就可以开始通信了:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) 
{    
    if(argc != 3)
    {
        perror("parameter");
        exit(-1);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

    if(connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) 
    {
        perror("connect");
        exit(1);
    }    

    while(1)
    {
        std::cout << "[client say]# ";
        std::string message;
        std::getline(std::cin, message);

        write(sockfd, message.c_str(), message.size());
        
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
        else
            break;
        
    }
    return 0;
}

那么以上就是三个文件的简单编写,不过我们是能发现,如果我们启动两个客户端,服务器就会阻塞,所以第一个版本的最大弊端,是不能够并发执行多个请求,它是串行的,那肯定不可以,我们需要进行改动。

TCPserver_v1--多进程版本

实际上我们要改动的只有loop函数,因为我们明显发现函数loop,当服务器accept成功的时候,就开始执行service的时候,如果该客户端不退出,服务器就要一直在这个循环里面为它服务,所以我们不能让服务器主体来服务客户端,应该让别人服务客户端,具体是哪个客户端呢?

我们可以让子进程来服务对吧?

// version--2 多进程版本
pid_t id = fork();
if (id == 0)
{
    // child
    ::close(_listensocket);
    if (fork() > 0)    exit(0);

    Service(sockfd); // 孙子进程执行
    exit(0);
}

    // ::close(sockfd);
 else
 {
        // father
        ::close(sockfd);
        int n = waitpid(id, nullptr, 0);

        if (n > 0) // 忽略最好
        std::cout << "wait success!" << std::endl;
 }

那么问题来了,如果我们让子进程来服务,那么父进程是不是需要等待子进程退出?并且接收到SIGCHLD信号然后去回收子进程?这样导致的问题是父进程仍然会因为等待子进程而阻塞,从而不能服务其他客户端,所以,我们需要使用双fork技巧。即让子进程再fork,创建孙子进程,让孙子进程执行服务,子进程创建成功就退出,这样父进程也不会阻塞,直接等待就成功了。

不过这里其实最好的方法是使用signal(SIGCHLD,SIG_IGN),即忽略信号,不等待,当然也可以使用的双fork技巧,都是可以的。反正最后总有一个进程是交给系统处理的。

当然了,在这里我们会涉及到系统层面的知识,即父进程子进程是共享文件描述符表的,这里我们建议,父进程关闭sockfd,子进程关闭listensockfd,因为也用不上,并且如果不小心误操作了,就会导致较为严重的后果,所以建议关闭。

以上就是第二个多进程版本。


TCPserver_v2--多线程版本

都有了多进程了,多线程不过分吧?

使用多线程的时候,我们要注意两个点:

1.线程之间是共享文件描述符表的,所以这里我们是一定不能关闭文件描述符
2.线程执行的函数默认参数是void* args,但是成员函数默认有参数this指针,所以需要使用static

这里就是两个最主要的问题,然后既然没有this指针,我们没有办法调用service,我们就需要单独创建一个类,用来接收TCPserver的字段,其实就是过渡一下没有this指针的问题:

  class ThreadData
    {
    public:
        int _sockfd;
        TcpServer *_self;

    public:
        ThreadData(int sockfd, TcpServer *self)
            : _sockfd(sockfd), _self(self)
        {
        }
    };

    void Loop()
    {
        // signal(SIGCHLD, SIG_IGN);
        bool _isrunning = true;
        while (_isrunning)
        {

            // accept
            sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = ::accept(_listensocket, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                std::cout << "accept error" << std::endl;
                continue;
            }
            // service

            // version--3 多线程版本
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, this);
            pthread_create(&tid, nullptr, Excute, td); // 1.成员函数默认有this指针
        }
    }
    static void *Excute(void *args)
    {
        pthread_detach(pthread_self()); // 为了让主线程不等待
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_self->Service(td->_sockfd);

        delete td;
        return nullptr;
    }

并且,为了防止内存泄露,我们这里千万不要忘了delete td,然后线程和进程一样,我们不希望主线程等待它,所以使用线程分离,它执行完了之后它自己释放就可以了。

以上就是多线程版本,其实我们能发现不管是什么版本,只是为了改动服务函数的调用而已。

TCP的第一个基本使用就到此结束了~~


感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【layout组件 与 路由镶嵌】vue3 后台管理系统

前言 很多同学在第一次搭建后台管理系统时&#xff0c;会遇到一个问题&#xff0c;layout组件该放哪里&#xff1f;如何使用&#xff1f;路由又该如何设计&#xff1f; 这边会讲一下我的思考过程和最后的结果&#xff0c;大家可以参考一下&#xff0c;希望大家看完能有所收获。…

mobile自动化测试-appium webdriverio

WebdriverIO是一款支持mobile app和mobile web自动化测试框架&#xff0c;与appium集成&#xff0c;完成对mobile应用测试。支持ios 和android两种平台&#xff0c;且功能丰富&#xff0c;是mobile app自动化测试首选框架。且官方还提供了mobile 应用测试example代码&#xff0…

Spring Bean有哪几种配置方式?

大家好&#xff0c;我是锋哥。今天分享关于【Spring Bean有哪几种配置方式&#xff1f;】面试题。希望对大家有帮助&#xff1b; Spring Bean有哪几种配置方式&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring Bean的配置方式主要有三种&#xff…

解析小米大模型MiMo:解锁语言模型推理潜力

一、基本介绍 1.1 项目背景 在大型语言模型快速发展的背景下,小米AI团队推出MiMo系列模型,突破性地在7B参数规模上实现卓越推理能力。传统观点认为32B以上模型才能胜任复杂推理任务,而MiMo通过创新的训练范式证明:精心设计的预训练和强化学习策略,可使小模型迸发巨大推理…

证券行业数字化转型:灵雀云架设云原生“数字高速路”

01 传统架构难承重负&#xff0c;云原生破局成必然 截至2024年&#xff0c;证券行业总资产突破35万亿元&#xff0c;线上交易占比达85%&#xff0c;高频交易、智能投顾等业务对算力与响应速度提出极限要求。然而&#xff0c;以虚拟化为主导的传统IT架构面临四大核心瓶颈&#…

Centos系统详解架构详解

CentOS 全面详解 一、CentOS 概述 CentOS&#xff08;Community Enterprise Operating System&#xff09; 是基于 Red Hat Enterprise Linux&#xff08;RHEL&#xff09; 源代码构建的免费开源操作系统&#xff0c;专注于稳定性、安全性和长期支持&#xff0c;广泛应用于服…

【后端】SpringBoot用CORS解决无法跨域访问的问题

SpringBoot用CORS解决无法跨域访问的问题 一、跨域问题 跨域问题指的是不同站点之间&#xff0c;使用 ajax 无法相互调用的问题。跨域问题本质是浏览器的一种保护机制&#xff0c;它的初衷是为了保证用户的安全&#xff0c;防止恶意网站窃取数据。但这个保护机制也带来了新的…

MySQL 8.0(主从复制)

MySQL 8.0 的 主从复制&#xff08;Master-Slave Replication&#xff09; 是一种数据库高可用和数据备份的核心技术&#xff0c;下面用 一、什么是主从复制&#xff1f; 就像公司的「领导-秘书」分工&#xff1a; 主库&#xff08;Master&#xff09;&#xff1a;负责处理所…

TCPIP详解 卷1协议 十 用户数据报协议和IP分片

10.1——用户数据报协议和 IP 分片 UDP是一种保留消息边界的简单的面向数据报的传输层协议。它不提供差错纠正、队列管理、重复消除、流量控制和拥塞控制。它提供差错检测&#xff0c;包含我们在传输层中碰到的第一个真实的端到端&#xff08;end-to-end&#xff09;校验和。这…

finebi使用资源迁移无法导入资源,解决方法

finebi使用资源迁移无法导入资源&#xff0c;解决方法 最近在使用finebi开发finebi报表&#xff0c;报表开发之后&#xff0c;从一台电脑将资源导入另一台电脑后&#xff0c;出现不允许导入的提示&#xff0c;如下&#xff1a; 原因&#xff1a; 两个finebi的管理员名称不一致…

分布式锁redisson的中断操作

1、先贴代码 RequestMapping(value "/update", method RequestMethod.POST)ResponseBodypublic Result update(RequestBody Employee employee) { // 修改数据库&#xff08;存在线程不安全 需要使用redison设置分布式锁 防止被修改&#xff09; // 设…

Docker:安装配置教程(最新版本)

文章目录 一、前言二、具体操作2.1 卸载 Docker (可选)2.2 重新安装&#xff08;使用清华大学镜像&#xff09;2.3 配置轩辕镜像加速2.4 Docker 基本命名2.5 测试是否成功 三、结语 一、前言 Docker 是一种容器化技术&#xff0c;在软件开发和部署中得到广泛的应用&#xff0c…

neo4j官方示例

目录 一、准备数据 1.执行查看结果 二、操作 1.find 单个节点 2.同上&#xff0c;已某个属性去查询 3. 指定查询个数 4.条件查询 5.查询某个人出演的电影汇总 6.查询tom出演的电影中&#xff0c;还有其他演员的信息。 7.查询跟电影(Cloud Atlas)有关的演员&#xff0…

前端自学入门:HTML 基础详解与学习路线指引

在互联网的浪潮中&#xff0c;前端开发如同构建数字世界的基石&#xff0c;而 HTML 则是前端开发的 “入场券”。对于许多渴望踏入前端领域的初学者而言&#xff0c;HTML 入门是首要挑战。本指南将以清晰易懂的方式&#xff0c;带大家深入了解 HTML 基础&#xff0c;并梳理前端…

vue实现与后台springboot传递数据【传值/取值 Axios 】

vue实现与后台springboot传递数据【传值/取值】 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是node.js和vue的使用。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性。【帮帮志系列文章】&#xff1a;每…

【英语笔记(三)】介绍谓语动词的分类,初步讲解四种基本状态:一般、进行、完成、完成进行

1. 五大类谓语动词 2. 谓语动词分类 3. 动词时间 过去--------------------------现在-----------------------未来 3. 动词状态 3.1 进行状态 3.2 完成状态 3.3 完成进行状态 3.4 一般状态 4. 时间 状态 名称说明例句现在现在现在现在进行时态现在某物正在做什么事情一只…

【Python】让Selenium 像Beautifulsoup一样,用解析HTML 结构的方式提取元素!

我在使用selenium的find_element的方式去获取网页元素&#xff0c;一般通过xpath、css_selector、class_name的方式去获取元素的绝对位置。 但是有时候如果网页多了一些弹窗或者啥之类的&#xff0c;绝对位置会发生变化&#xff0c;使用xpath等方法&#xff0c;需要经常变动。…

2025 后端自学UNIAPP【项目实战:旅游项目】3、API接口请求封装,封装后的简单测试以及实际使用

一、创建请求封装目录 选中自己的项目&#xff0c;右键鼠标---->新建---->目录---->名字自定义【我的是api】 二、创建两个js封装文件 选中封装的目录&#xff0c;右键鼠标---->新建---->js文件---->名字自定义【我的两个js文件分别是my_http和my_api】 三…

Ascend的aclgraph(二)_npu_backend中还有些什么秘密?

1 _npu_backend 文章还是从代码开始 import torch_npu, torchair config torchair.CompilerConfig() # 设置图下沉执行模式 config.mode "reduce-overhead" npu_backend torchair.get_npu_backend(compiler_configconfig) opt_model torch.compile(model, back…

ventoy安全启动怎么选_ventoy安全启动支持是开还是关

ventoy安全启动怎么选&#xff1f;Ventoy新一代多系统启动U盘解决方案。国产开源U盘启动制作工具&#xff0c;支持Legacy BIOS和UEFI模式&#xff0c;理论上几乎支持任何ISO镜像文件&#xff0c;支持加载多个不同类型的ISO文件启动&#xff0c;无需反复地格式化U盘&#xff0c;…