【计网】基于TCP协议的Echo Server程序实现与多版本测试

目录

前言:

1、InitServer类的实现

1.1. 创建流式套接字

1.2. bind 绑定一个固定的网络地址和端口号

1.3.listen监听机制

1.4.完整代码

2. 循环接收接口与服务接口

2.1.accept函数讲解

讲个商场拉客的故事方便我们理解:

2.2.服务接口实现

3.服务端和客户端的实现

3.1.服务端

3.2.客户端

connect函数讲解:

4.多版本测试运行

4.1.单进程处理

4.2.多进程处理

测试结果:

4.3.多线程处理

代码:​编辑

前言:

我们上一篇文章讲解了利用udp协议实现一个简单的echo_server程序,将客户端的数据在服务端打印出来UDP协议。今天我们来讲解利用TCP协议实现的一个简单的echo_server程序,认识并熟悉各个接口的功能以及如何使用!

1、InitServer类的实现

1.1. 创建流式套接字

socket函数讲解:

  • socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用 read/write 在网络上收发数据;
  • 如果 socket()调用出错则返回-1;
  • 对于 IPv4, family 参数指定为 AF_INET;
  • 对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
  • protocol 参数的介绍从略,指定为 0 即可。

该部分与udp唯一的区别就是type参数的不同,因为对于TCP协议是面向流的传输协议,所以参数指定为SOCK_STREAM

代码:

1.2. bind 绑定一个固定的网络地址和端口号

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;
  • bind()成功返回 0,失败返回-1。
  • bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;
  • struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度;

注意这部分代码与udp是一模一样的。

代码:

1.3.listen监听机制

listen函数讲解

  • listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5)
  • listen()成功返回 0,失败返回-1;

tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的。tcpserver 启动,未来首先要一直等待客户的连接到来,所以需要listen函数进行监听。

代码:

1.4.完整代码

void InitServer()
    {
        // 1. 创建流式套接字
        _listensock = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        LOG(DEBUG, "socket create success, sockfd is : %d\n", _listensock);

        // 2. bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        int n = ::bind(_listensock, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        LOG(DEBUG, "bind success, sockfd is : %d\n", _listensock);

        // 3. tcp是面向连接的,所以通信之前,必须先建立连接。服务器是被连接的
        //    tcpserver 启动,未来首先要一直等待客户的连接到来
        n = ::listen(_listensock, gbacklog);
        if (n < 0)
        {
            LOG(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }
        LOG(DEBUG, "listen success, sockfd is : %d\n", _listensock);
    }

2. 循环接收接口与服务接口

Loop()循环循环接收接口需要:

  1. 不断从套接字文件中accept获取连接流与客户端信息!
  2. 获取成功后,就可以进行服务了
  3. 服务就是从流中读取数据,然后处理之后再写回流中!!!使用的接口是read与write,文件流中我们对他们很熟悉!!!

2.1.accept函数讲解

  • 三次握手完成后, 服务器调用 accept()接受连接;
  • 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给 addr 参数传 NULL,表示不关心客户端的地址;

accept返回的套接字和传入的参数套接字到底有什么关系?

传入的套接字这个参数不是真正参与通信的,只是用来建立连接的。我们真正用来与客户端通信的是返回的套接字

讲个商场拉客的故事方便我们理解:

就比如我们在商场逛街,很多门面前面有拉客的,拉客的人在马路上拉客,然后接到客人之后,会从店里再喊一个服务员用来服务新拉过来的客人。因此拉客的人就是我们的参数,而返回值的套接字才是真正服务我们的,也就是说如果有多个客人,就会有很多个返回值的套接字,分别用来服务。

部分代码:

2.2.服务接口实现

服务就是从流中读取数据,然后处理之后再写回流中。使用的接口是read与write。

void Service(int sockfd, InetAddr client)
    {
        LOG(DEBUG, "get a new link, info %s:%d, fd : %d\n", client.Ip().c_str(), client.Port(), sockfd);

        std::string clientaddr = "[" + client.Ip() + ":" + std::to_string(client.Port()) + "]# ";
        while (true)
        {
            char inbuffer[1024];
            ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << clientaddr << inbuffer << std::endl;

                std::string echo_string = "[server echo]# ";
                echo_string += inbuffer;

                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                // client 退出&&关闭连接了
                LOG(INFO, "%s quit\n", clientaddr.c_str());
                break;
            }
            else
            {
                LOG(ERROR, "read error\n", clientaddr.c_str());
                break;
            }
        }
	std::cout << "server开始退出" << std::endl;
	shutdown(sockfd, SHUT_RD);
	std::cout << "shut _ rd " << std::endl;
	sleep(10);
	//shutdown(sockfd, SHUT_WR);
	//std::cout << "shut _ wr " << std::endl;
        //::close(sockfd); // 文件描述符泄漏
    }

3.服务端和客户端的实现

3.1.服务端

服务端简单的创建一个服务器类然后进行初始化和loop就可以了。

代码:

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

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " local_port\n" << std::endl;
}

// ./tcpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    EnableScreen();
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    tsvr->InitServer();
    tsvr->Loop();

    return 0;
}

3.2.客户端

  1. 首先根据传入的参数进行初始化服务器IP地址和端口号
  2. 然后创建套接字文件 ,并进行connect连接绑定bind,客户端回被动绑定一个端口号。
  3. 绑定成功之后就可以通过sockfd进行写入与读取了。

connect函数讲解:

  • 客户端需要调用 connect()连接服务器;
  • connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址;
  • connect()成功返回 0,出错返回-1;

代码:

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // tcp client 要bind,不要显示的bind.
    struct sockaddr_in server;
    // 构建目标主机的socket信息
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    while(true)
    {
        std::cout << "Please Enter# ";
        std::string outstring;
        std::getline(std::cin, outstring);

        ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0); //write
        if(s > 0)
        {
            char inbuffer[1024];
            ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer)-1, 0);
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer<< std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }
    shutdown(sockfd, SHUT_WR);
    //::close(sockfd);
    return 0;
}

4.多版本测试运行

4.1.单进程处理

一次只能处理一个请求,显然这个版本是很废的,根本不满足用户需求,直接跳过

测试结果:

4.2.多进程处理

子进程会继承父进程的文件描述符表,所以子进程一定会看到父进程之前创建的sockfd,父子进程的文件描述符表是独立的,子进程会拷贝父进程的那一份,线程才会共享

  • 对于子进程:关心sockfd, 不关心listensock,所以对于子进程需要关闭listensock
  • 对于父进程:关心listensock,不关心sockfd,所以对于父进程需要关闭sockfd

为什么父进程要关闭sockfd,不然一直创建会导致sockfd一直减少,浪费资源

子进程还会创建子进程,就是孙子进程,我们不进行任何处理,那么这个孙子进程就变成了孤儿进程,系统自己领养进行运行,而父进程仍在源源不断的创建新的子进程,那么这个版本服务器就可以并发处理了!

代码:

            // Version 1: 采用多进程
            pid_t id = fork();
            if (id == 0)
            {
                // child : 关心sockfd, 不关心listensock
                ::close(_listensock); // 建议
                if(fork() > 0) exit(0); 
                Service(sockfd, InetAddr(peer)); //孙子进程 -- 孤儿进程 --- 系统领养
                exit(0);
            }

            // father: 关心listensock,不关心sockfd
            ::close(sockfd);
            waitpid(id, nullptr, 0);

测试结果:

测试成功!但是我们知道切换进程时,CPU会切换上下文和热点数据。在并发场景下多进程的不断切换会消耗大量的性能!

而作为轻量级进程的线程就可以避免这样的问题!

4.3.多线程处理

我们采用线程分离的方法来实现并发处理!

多线程禁止关闭文件描述符!因为多线程是共享文件描述符表的,如果直接将sockfd关掉了,那么先创建的线程可能无法通过文件描述符来读取数据。

在服务器编程中,为了处理大量的客户端请求,可以采用多线程技术来提高服务器的并发性能。在这种情况下,线程分离可以确保每个子线程在结束后自动释放资源,从而避免资源泄露,提高服务器的稳定性和性能

代码:

测试结果:非常nice!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Latex公式转换编辑网站

https://editor.codecogs.com/ https://www.latexlive.com/home## https://simpletex.cn/ai/latex_ocr https://webdemo.myscript.com/views/math/index.html# 参考 https://latex.91maths.com/ https://web.baimiaoapp.com/image-to-latex https://blog.csdn.net/qq_45100…

多语言电商系统的多语言设计机制

在全球化电商市场中&#xff0c;跨语言沟通是提升用户体验和扩大市场份额的关键。为了满足不同语言用户的需求&#xff0c;构建一个支持多语言的电商系统已成为企业扩展国际市场的重要步骤。多语言电商系统需要能够根据用户的语言偏好自动显示内容&#xff0c;同时保证翻译的准…

VBA08-if语句

一、单行 If 语句 If x > 10 Then MsgBox "x is greater than 10"二、多行 If...Then...End If 语句 If x > 10 ThenMsgBox "x is greater than 10"y x 5 End If 三、If...Then...Else 语句 If condition Then 当条件为真时执行的代码块stateme…

Python数据可视化seaborn

产品经理在做数据分析时可能需要通过可视化来分析。seaborn官网 1. relplot 散点图 https://seaborn.pydata.org/examples/scatterplot_sizes.html import pandas as pd import seaborn as sns df pd.DataFrame({x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],y: [8, 6, 7, 8, 4, 6,…

【数据库系列】postgresql链接详解

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

webpack 执行流程 — 实现 myWebpack

前言 实现 myWebpack 主要是为了更好的理解&#xff0c;webpack 中的工作流程&#xff0c;一切都是最简单的实现&#xff0c;不包含细节内容和边界处理&#xff0c;涉及到 ast 抽象语法树和编译代码部分&#xff0c;最好可以打印出来观察一下&#xff0c;方便后续的理解。 re…

Hadoop生态圈框架部署(五)- Zookeeper完全分布式部署

文章目录 前言一、Zookeeper完全分布式部署&#xff08;手动部署&#xff09;1. 下载Zookeeper2. 上传安装包2. 解压zookeeper安装包3. 配置zookeeper配置文件3.1 创建 zoo.cfg 配置文件3.2 修改 zoo.cfg 配置文件3.3 创建数据持久化目录并创建myid文件 4. 虚拟机hadoop2安装并…

Python小白学习教程从入门到入坑------第二十九课 访问模式(语法进阶)

目录 一、访问模式 1.1 r 1.2 w 1.3 1.3.1 r 1.3.2 w 1.3.3 a 1.4 a 一、访问模式 模式可做操作若文件不存在是否覆盖r只能读报错-r可读可写报错是w只能写创建是w可读可写创建是a只能写创建否&#xff0c;追加写a可读可写创建否&#xff0c;追加写 1.1 r r&…

巡检任务管理系统(源码+文档+部署+讲解)

本文将深入解析“巡检任务管理系统”的项目&#xff0c;探究其架构、功能以及技术栈&#xff0c;并分享获取完整源码的途径。 系统概述 巡检任务管理、巡检抽查、巡检任务随机分派等功能 本项目名称为巡检管理系统&#xff0c;是对巡检工作进行数字化管理的系统。该系统适用…

自动驾驶系列—自动驾驶车辆的姿态与定位:IMU数据在复杂环境中的关键作用

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

如何运营Github Org

目录 前言 正文 关于分支保护 特别说明 如何在Windows环境下配置GitHub Desktop GPG签名&#xff1f; 推荐分支保护选择 关于good first issue 如何设置good first issue&#xff1f; 关于Project 尾声 &#x1f52d; Hi,I’m Pleasure1234&#x1f331; I’m currently learni…

odrive代码阅读笔记

电机参数 电流环带宽 atan2 #include "float.h" #define MACRO_MAX(x, y) (((x) > (y)) ? (x) : (y)) #define MACRO_MIN(x, y) (((x) < (y)) ? (x) : (y)) #define f_abs(x) ((x > 0) ? x : -x) // based on https://math.stackexchange.com/a/11050…

笔记本怎么开启TPM2.0_笔记本开启TPM2.0教程(不同笔记本开启tpm2.0方法)

在win11最低要求是提示&#xff0c;电脑必须满足 TPM 2.0&#xff0c;并开需要开启TPM 才能正常安装windows11系统&#xff0c;有很多笔记本的用户问我&#xff0c;笔记本怎么开启tpm功能呢&#xff1f;下面小编就给大家详细介绍一下笔记本开启tpm功能的方法。 如何确认你笔记本…

ModuleNotFoundError: No module named ‘_ssl‘ centos7中的Python报错

报错 ModuleNotFoundError: No module named ‘_ssl’ 解决步骤&#xff1a; 1.下载openssl wget https://www.openssl.org/source/openssl-3.0.7.tar.gz tar -zxvf openssl-3.0.7.tar.gz cd openssl-3.0.72.编译安装 ./config --prefix/usr/local/openssl make make install3…

迁移学习相关基础

迁移学习 目标 将某个领域或任务上学习到的知识或模式应用到不同但相关的领域或问题中。 主要思想 从相关领域中迁移标注数据或者知识结构、完成或改进目标领域或任务的学习效果。 概述 Target data&#xff1a;和你的任务有直接关系的数据&#xff0c;但数据量少&#xff…

ReactPress系列—Next.js 的动态路由使用介绍

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎提出宝贵的建议&#xff0c;感谢Star。 Next.js 的动态路由使用介绍 Next.js 是一个流行的 React 框架&#xff0c;支持服务端渲染、静态站点生成和动态路由等功能&#xff0c;极大地简化…

一文熟悉新版llama.cpp使用并本地部署LLAMA

0. 简介 最近是快到双十一了再给大家上点干货。去年我们写了一个大模型的系列&#xff0c;经过一年&#xff0c;大模型的发展已经日新月异。这一次我们来看一下使用llama.cpp这个项目&#xff0c;其主要解决的是推理过程中的性能问题。主要有两点优化&#xff1a; llama.cpp …

yolov8涨点系列之轻量化主干网络替换

文章目录 YOLOv8 替换成efficientvit轻量级主干网络的好处计算效率提升模型部署更便捷方便模型移植 模型可扩展性增强便于集成其他模块支持模型压缩技术 主干网络替换1.创建yolov8_efficeintVit.py2.修改task.py(1)引入创建的efficientViT文件(2)修改_predict_once函数(3)修改p…

python代码打包exe文件(可执行文件)

一、exe打包 1、构建虚拟环境 conda create -n env_name python3.8 #env_name,python根据自己需求修改2、保存和安装项目所需的所有库 pip freeze > requirements.txt3、虚拟环境安装项目包、库 pip install -r requirements.txt4、安装pyinstaller pip install pyinst…

scala学习记录,Set,Map

set&#xff1a;集合&#xff0c;表示没有重复元素的集合&#xff0c;特点&#xff1a;唯一 语法格式&#xff1a;val 变量名 Set [类型]&#xff08;元素1&#xff0c;元素2...&#xff09; 可变不可变 可变&#xff08;mutable&#xff09;可对元素进行添加&#xff0c;删…