【Linux】Socket编程—TCP

🔥 个人主页:大耳朵土土垚
🔥 所属专栏:Linux系统编程

这里将会不定期更新有关Linux的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉

文章目录

  • 1. TCP socket API 详解
    • socket()
    • bind()
    • listen()
    • accept()
    • connect()
  • 2. Echo Server
    • TCP服务器
      • 多进程版本
      • 多线程版本
      • 线程池版本
    • TCP客户端

1. TCP socket API 详解

  下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。

socket()

在这里插入图片描述

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

bind()

在这里插入图片描述

  • 介绍:服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号;

  • 返回值: bind()成功返回 0,失败返回-1。

  • 作用:将参数 sockfdmyaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号;

  • 参数: 前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度;我们的程序中对 myaddr 参数是这样初始化的:
    在这里插入图片描述
      1. 将整个结构体清零;
      2. 设置地址类型为 AF_INET;
      3. 网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有
    多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听, 直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址;
      4. 端口号为 SERV_PORT, 我们定义为 8080;

listen()

在这里插入图片描述

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

accept()

在这里插入图片描述

  • 介绍:三次握手完成后, 服务器调用 accept()接受连接; 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • 参数:addr 是一个传出参数,accept()返回时传出客户端的地址和端口号; 如果给 addr 参数传 NULL,表示不关心客户端的地址; addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

我们的服务器程序结构是这样的:
在这里插入图片描述

  • 返回值:sockfd用来进行通信

connect()

在这里插入图片描述

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

2. Echo Server

  有了上面的接口,我们就可以实现以TCP为基础的简单消息回显服务器了,运行结果应该如下图所示:

在这里插入图片描述
代码如下:

TCP服务器

#pragma once

#include <iostream>
#include <string.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"

#define BACKLOG 8
using namespace InetAddrModule;
using namespace LogModule;
static const uint16_t defaultport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = defaultport) : _port(port), _listensockfd(-1), _isruning(false)
    {
    }
    void InitServer()
    {
        // 1.创建Tcp套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer socket fail ...";
            Die(SOCKET_ERR);
        }

        // 填充信息
        struct sockaddr_in serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_port = ::htons(_port);//aaa注意要转网络!!!!!!!!!!
        serveraddr.sin_addr.s_addr = INADDR_ANY; // 表示可以接收任意地址的信息

        // 2. bind;
        int n = ::bind(_listensockfd, CONV(&serveraddr), sizeof(serveraddr));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer bind fail ...";
            Die(BIND_ERR);
        }

        // 3.监听
        int m = ::listen(_listensockfd, BACKLOG);
        if (m < 0)
        {
            LOG(LogLevel::ERROR) << "InitServer listen fail ...";
            Die(LISTEN_ERR);
        }

        LOG(LogLevel::INFO) << "ServerInit success...";
    }
    void handler(int sockfd)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                LOG(LogLevel::INFO) << buffer;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                ::write(sockfd, echo_string.c_str(),echo_string.size());
            }
            else if (n == 0) // client 退出
            {
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else
            {
                // 读取失败
                break;
            }
        }
        ::close(sockfd); // fd泄漏问题!
    }

    void Start()
    {
        _isruning = true;
        while(_isruning)
        {
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!
            LOG(LogLevel::DEBUG) << "accepting ...";
            // 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);
            
            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "StartServer accept fail ...";
                continue; // 继续接收
            }
            LOG(LogLevel::INFO)<<"ServerStart success...";
            // 连接成功后就可以通信
            handler(sockfd);
        }
        _isruning = false;
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isruning;
};               

与Udp服务器不同的是,Tcp服务要求我们先调用listen接口监听,然后在通过accept和客户端使用connet建立连接后才可以进行通信;所以如果仅仅使用单进程是无法满足同时接收多个客户端的消息,下面将会给出多进程、多线程以及基于线程池实现的Tcp服务。

多进程版本

//其他的不变
void Start()
    {
        _isruning = true;
        while(_isruning)
        {
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!
            LOG(LogLevel::DEBUG) << "accepting ...";
            // 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);
            
            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "StartServer accept fail ...";
                continue; // 继续接收
            }
            LOG(LogLevel::INFO)<<"ServerStart success...";
            // 连接成功后就可以通信

            //version1: 多进程
            pid_t id = ::fork();
            if(id == 0)//子进程
            {
                ::close(_listensockfd);//要关掉不需要的文件描述符,避免fd泄露问题
                if(fork())//子进程再创建孙子进程
                    ::exit(0);//让子进程退出,孙子进程成为孤儿进程,这样就不用父进程回收
                //孙子进程处理,结束后由操作系统回收
                handler(sockfd);
                ::exit(0);
            }
            ::close(sockfd);
            //子进程退出后,父进程就不会阻塞在这里,继续接收其他客户端连接
            int rid = ::waitpid(id, nullptr, 0);
            if(rid < 0)
                 LOG(LogLevel::WARNING) << "ServerStart waitpid error...";
            
        }
        _isruning = false;
    }

对于多进程,首先每个进程都有自己的文件描述符表,所以父子进程都需要关闭自己不需要的文件描述符;

其次父进程需要等待回收子进程,此时父进程会阻塞直到子进程完成通信,这样和之前单进程通信效果一样,所以为了不让父进程阻塞,子进程需要再创建子进程,用它来完成通信,此时父进程就可以直接回收子进程,孙子进程就成为孤儿进程进行通信,结束后由操作系统回收。

多线程版本

    struct ThreadData
    {
        int sockfd;
        TcpServer *self;
    };
    
static void *ThreadEntry(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离,线程执行结束后自动被系统回收
        ThreadData *data = (ThreadData *)args;
        data->self->handler(data->sockfd);
        return nullptr;
    }

    void Start()
    {
        _isruning = true;
        while (_isruning)
        {
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!
            LOG(LogLevel::DEBUG) << "accepting ...";
            // 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);

            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "StartServer accept fail ...";
                continue; // 继续接收
            }
            LOG(LogLevel::INFO) << "ServerStart success...";
            // 连接成功后就可以通信
            // version2: 多线程
            // 主线程和新线程共享一张文件描述符表
            pthread_t tid;
            ThreadData *data = new ThreadData;
            data->sockfd = sockfd;
            data->self = this;
            pthread_create(&tid, nullptr, ThreadEntry, data);
}
        _isruning = false;
    }

设置线程分离这样线程执行完毕后就可以自动被系统回收

线程池版本

using task_t = std::function<void()>;

void Start()
    {
        _isruning = true;
        while (_isruning)
        {
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer); // 这个地方一定要注意,要不然,会有问题!
            LOG(LogLevel::DEBUG) << "accepting ...";
            // 我们要获取client的信息:数据(sockfd)+client socket信息(accept || recvfrom)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);

            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "StartServer accept fail ...";
                continue; // 继续接收
            }
            LOG(LogLevel::INFO) << "ServerStart success...";

            // version-3:线程池版本 比较适合处理短任务,或者是用户量少的情况
            ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd]()
                                                      { this->handler(sockfd); });
            }
        _isruning = false;
    }

引入之前实现的线程池,并使用单例模式

使用服务器代码如下:

#include "TcpServer.hpp"

int main()
{
    std::unique_ptr<TcpServer> tcpserver = std::make_unique<TcpServer>();
    tcpserver->InitServer();
    tcpserver->Start();
    return 0;
}

TCP客户端

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

#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;
using namespace InetAddrModule;

int sockfd = -1;

//./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        LOG(LogLevel::ERROR) << "Usage:" << argv[0] << " serverip serverport";
        Die(ARGV_ERR);
    }

    // 1.创建sockfd
    sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::WARNING) << "client sockfd fail...";
        Die(SOCKET_ERR);
    }

    // 2.填充服务器信息
     std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
   // InetAddr serveraddr(serverip, serverport); 
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(serverport);
    serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str());

    // 3.与服务器建立连接
    int n = ::connect(sockfd, CONV(&serveraddr), sizeof(serveraddr));
    if (n < 0)
    {
        LOG(LogLevel::ERROR) << "ClientConnet fail...";
        Die(CONNET_ERR);
    }

    // 4. 发送请求给服务器
    while (true)
    {
        // 4.1获取信息
        std::cout << "Please Enter# ";
        std::string message;
        std::getline(std::cin, message);

        // 4.2发送信息给服务器
        ssize_t n = ::sendto(sockfd, message.c_str(), sizeof(message), 0, CONV(&serveraddr), sizeof(serveraddr));
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "client sendto fail...";
            continue;
        }

        // 4.3从服务器接收信息
        char buffer[1024];
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t m = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&tmp), &len);

        if (m > 0)
        {
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
        else
        {
            LOG(LogLevel::ERROR) << "client recvfrom fail...";
        }
    }
    ::close(sockfd);
    return 0;
}


与UDP客户端相比,TCP客户端需要与服务器通过connet连接后才能通信。

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

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

相关文章

001 SpringCloudAlibaba整合 - Nacos注册配置中心、Sentinel流控、Zipkin链路追踪、Admin监控

SpringCloudAlibaba 文章目录 SpringCloudAlibaba1.版本依赖关系2022.x 分支2021.x 分支2.2.x 分支 组件版本关系 2.基础项目构建1.引入全局pom文件2.创建对应的模块 3.SpringBootAdmin监控服务整合1.cloud-admin服务搭建1.导入服务端依赖2.主启动类添加EnableAdminServer注解启…

电动汽车电池监测平台系统设计(论文+源码+图纸)

1总体设计 本次基于单片机的电池监测平台系统设计&#xff0c;其整个系统架构如图2.1所示&#xff0c;其采用STC89C52单片机作为控制器&#xff0c;结合ACS712电流传感器、TLC1543模数转换器、LCD液晶、DS18B20温度传感器构成整个系统&#xff0c;在功能上可以实现电压、电流、…

DeepSeek从入门到精通:提示词设计的系统化指南

目录 引言&#xff1a;AIGC时代的核心竞争力 第一部分 基础篇&#xff1a;提示词的本质与核心结构 1.1 什么是提示词&#xff1f; 1.2 提示词的黄金三角结构 第二部分 类型篇&#xff1a;提示词的六大范式 2.1 提示语的本质特征 2.2 提示语的类型 2.2.1 指令型提示词 …

【VB语言】EXCEL中VB宏的应用

【VB语言】EXCEL中VB宏的应用 文章目录 [TOC](文章目录) 前言一、EXCEL-VB1.实验过程2.代码 二、EXCEL-VB 生成.c.h文件1.实验过程2.代码 四、参考资料总结 前言 1.WPS-VB扩展包 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、EXCEL-VB 1.实验过…

Redis7.0八种数据结构底层原理

导读 本文介绍redis应用数据结构与物理存储结构,共八种应用数据结构和 一. 内部数据结构 1. sds sds是redis自己设计的字符串结构有以下特点: jemalloc内存管理预分配冗余空间二进制安全(c原生使用\0作为结尾标识,所以无法直接存储\0)动态计数类型(根据字符串长度动态选择…

NixHomepage - 简单的个人网站

&#x1f4bb; NixHomepage - 简单的个人网站 推荐下个人的开源项目&#xff0c;演示网站&#xff0c;项目链接 https://github.com/nixgnauhcuy/NixHomepage&#xff0c;喜欢的话可以为我的项目点个 Star~ &#x1f4f7; 预览 ⚙️ 功能特性 多平台适配 明亮/暗黑模式切换 W…

给压缩文件加密码的5种方法(win/mac/手机/网页端)

把文件加密压缩&#xff0c;一方面能有效保护个人隐私与敏感信息&#xff0c;防止数据在传输或存储过程中被窃取、篡改。另一方面&#xff0c;压缩文件可减少存储空间占用&#xff0c;提升传输速度&#xff0c;方便数据的存储与分享。以下为你介绍5种常见的加密压缩方法。 一、…

如何通过AI轻松制作PPT?让PPT一键生成变得简单又高效

如何通过AI轻松制作PPT&#xff1f;让PPT一键生成变得简单又高效&#xff01;在这个信息化飞速发展的时代&#xff0c;PPT已经成为我们日常工作、学习和生活中不可或缺的一部分。无论是公司会议、学术报告&#xff0c;还是个人展示&#xff0c;PPT的作用都不容忽视。很多人对于…

Linux之【网络I/O】前世今生(二)

前文回顾 通过学习 Linux之【网络I/O】前世今生&#xff08;一&#xff09;&#xff0c;我们知道了I/O 请求可以分为两个阶段&#xff0c;分别为 I/O 调用和 I/O 执行&#xff1a; I/O 调用 即用户进程向内核发起系统调用(通过 0x80 中断)。 I/O 执行 内核等待 I/O 请求处理完…

Redis未授权访问漏洞导致getshell

一、漏洞信息 redis默认情况下会绑定在本地6379端口&#xff0c;如果没有进行采用相关的策略&#xff0c;就会将redis服务暴露到公网上&#xff0c;如果再没有设置密码认证(一般为空)的情况下&#xff0c;会导致任意用户可以访问到目标服务器的情况下未授权访问redis以及读取r…

伯克利 CS61A 课堂笔记 08 —— Strings and Dictionaries

本系列为加州伯克利大学著名 Python 基础课程 CS61A 的课堂笔记整理&#xff0c;全英文内容&#xff0c;文末附词汇解释。 目录 01 Strings 字符串 Ⅰ Strings are An Abstraction. Ⅱ Strings Literals have Three Forms Ⅲ String are Sequences 02 Dictionaries 字典 …

【Stable Diffusion模型测试】测试ControlNet,没有线稿图?

相信很多小伙伴跟我一样&#xff0c;在测试Stable Diffusion的Lora模型时&#xff0c;ControlNet没有可输入的线稿图&#xff0c;大家的第一反应就是百度搜&#xff0c;但是能从互联网上搜到的高质量线稿图&#xff0c;要么收费&#xff0c;要么质量很差。 现在都什么年代了&a…

智能手表表带圆孔同心度检测

在智能手表的制造工艺中&#xff0c;表带圆孔同心度检测是确保产品品质的关键环节。精准的同心度不仅关乎表带与表体的完美适配&#xff0c;更直接影响用户的佩戴舒适度和产品的整体美观度。稍有偏差&#xff0c;就可能导致表带安装困难、佩戴时出现晃动&#xff0c;甚至影响智…

基于SSM+uniapp的数学辅导小程序+LW示例参考

1.项目介绍 系统角色&#xff1a;管理员、普通用户功能模块&#xff1a;用户管理、学习中心、知识分类管理、学习周报管理、口算练习管理、试题管理、考试管理、错题本等技术选型&#xff1a;SSM&#xff0c;Vue&#xff08;后端管理web&#xff09;&#xff0c;uniapp等测试环…

基于 openEuler 构建 LVS-DR 群集

一、 对比 LVS 负载均衡群集的 NAT 模式和 DR 模式&#xff0c;比较其各自的优势 。 二、 基于 openEuler 构建 LVS-DR 群集。 一 NAT 模式 部署简单&#xff1a;NAT 模式下&#xff0c;所有的服务器节点只需要连接到同一个局域网内&#xff0c;通过负载均衡器进行网络地址转…

JS设计模式之单例原型

那么单例模式都有哪些应用场景呢&#xff1f;如何通过构造函数创建单例如何使用模块化的方式创建总结 各位老铁们&#xff0c;今天我们介绍一下JS中单例设计模式&#xff0c;它的特点是确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取该实例&#xff08;无论被创…

vue+springboot+webtrc+websocket实现双人音视频通话会议

前言 最近一些时间我有研究&#xff0c;如何实现一个视频会议功能&#xff0c;但是找了好多资料都不太理想&#xff0c;最终参考了一个文章 WebRTC实现双端音视频聊天&#xff08;Vue3 SpringBoot&#xff09; 只不过&#xff0c;它的实现效果里面只会播放本地的mp4视频文件&…

Linux 基础IO——重定向和缓冲区

目录 一、重定向 1、重定向的本质 2、使用 dup2 系统调用 &#xff08;1&#xff09;输出重定向 &#xff08;2&#xff09;追加重定向 (3) 输入重定向 ​ 二、缓冲区 1.理解缓冲区 2.缓冲区刷新问题 3.为什么要有缓冲区&#xff1f; 4.这个缓冲区在哪里&#xff…

14、deepseek视觉大模型Janus Pro本地部署及实战

1、简介 2025.01.27&#xff1a; Janus-Pro发布&#xff0c;Janus的高级版本&#xff0c;显著提高了多模态理解和视觉生成。 Janus-Pro 是 Janus 的高级版本。具体来说&#xff0c; Janus-Pro 包括以下改进&#xff1a;优化的训练策略、 扩展的训练数据以及更大规模的模型。通…

【第3章:卷积神经网络(CNN)——3.1 CNN的基本结构与工作原理】

嘿,小伙伴们,今天咱们来聊聊深度学习里的一大明星——卷积神经网络(CNN)。这东西在图像识别、视频处理等领域简直不要太火,甚至人脸识别、物体检测这些高大上的应用,都离不开它的身影。废话不多说,咱们这就开聊! 一、CNN是什么东东? 在人工智能领域,卷积神经网络(…