【Linux】Socket编程—TCP

news2025/2/19 16:31:15
🔥 个人主页:大耳朵土土垚
🔥 所属专栏: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连接后才能通信。

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

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

相关文章

新数据结构(9)——Java异常体系

异常的种类 程序本身通常无法主动捕获并处理错误&#xff08;Error&#xff09;&#xff0c;因为这些错误通常表示系统级的严重问题&#xff0c;但程序可以捕获并处理异常&#xff08;Excrption&#xff09;&#xff0c;而Error则被视为一种程序无法或不应尝试恢复的异常类型。…

NixHomepage - 简单的个人网站

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

HCIA项目实践---OSPF的知识和原理总结

9.5 OSPF 9.5.1 从哪些角度评判一个动态路由协议的好坏&#xff1f; &#xff08;1&#xff09;选路佳&#xff08;是否会出环&#xff09; OSPF 协议采用链路状态算法&#xff0c;通过收集网络拓扑信息来计算最短路径&#xff0c;从根本上避免了路由环路的产生。 &#xff08…

Calico网络组件本地部署支持IPv6(Kubernetes)

知其然 问题背景 因项目现场的网络正逐步从IPv4向IPv6迁移&#xff0c;这几年现场服务器基本上都配置了双栈&#xff1b;但随着IPv6铺开&#xff0c;出现了很多纯IPv6的服务器&#xff0c;并且要求通信优先使用IPv6。 在项目建设之初&#xff0c;其实就考虑了上述情况&#…

【广州大学主办,发表有保障 | IEEE出版,稳定EI检索,往届见刊后快至1个月检索】第二届电气技术与自动化工程国际学术会议 (ETAE 2025)

第二届电气技术与自动化工程国际学术会议 (ETAE 2025) The 2nd International Conference on Electrical Technology and Automation Engineering 大会官网&#xff1a;http://www.icetae.com/【更多详情】 会议时间&#xff1a;2025年4月25-27日 会议地点&#xff1a…

Python项目31:待办事项列表应用1.0(命令行界面+Json+类+初学者必做)

------------★Python练手项目源码★------------ Python项目27&#xff1a;用Tkinter写日志管理系统&#xff08;中下等难度&#xff09; Python项目26&#xff1a;设计学生成绩管理系统&#xff08;简易版&#xff09; Python项目25&#xff1a;带滚动效果的商场抽奖系统&…

Redis 01 02章——入门概述与安装配置

一、入门概述 &#xff08;1&#xff09;是什么 Redis&#xff1a;REmote Dictionary Server&#xff08;远程字典服务器&#xff09;官网解释&#xff1a;Remote Dictionary Server(远程字典服务)是完全开源的&#xff0c;使用ANSIC语言编写遵守BSD协议&#xff0c;是一个高…

Large Language Model Distilling Medication Recommendation Model

摘要&#xff1a;药物推荐是智能医疗系统的一个重要方面&#xff0c;因为它涉及根据患者的特定健康需求开具最合适的药物。不幸的是&#xff0c;目前使用的许多复杂模型往往忽视医疗数据的细微语义&#xff0c;而仅仅严重依赖于标识信息。此外&#xff0c;这些模型在处理首次就…

2025最新版Node.js下载安装~保姆级教程

1. node中文官网地址&#xff1a;http://nodejs.cn/download/ 2.打开node官网下载压缩包&#xff1a; 根据操作系统不同选择不同版本&#xff08;win7系统建议安装v12.x&#xff09; 我这里选择最新版win 64位 3.安装node ①点击对话框中的“Next”&#xff0c;勾选同意后点…

springboot如何将lib和jar分离

遇到一个问题&#xff0c;就是每次maven package或者maven install后target中的jar很大&#xff0c;少的50几MB&#xff0c;大的100多兆 优化前&#xff1a; 优化后&#xff1a; 优化前 优化后压缩率77.2MB4.65MB93% 具体方案&#xff1a; pom.xml中 <build><…

RocketMQ和Kafka如何实现顺序写入和顺序消费?

0 前言 先说明kafka&#xff0c;顺序写入和消费是Kafka的重要特性&#xff0c;但需要正确的配置和使用方式才能保证。本文需要解释清楚Kafka如何通过分区来实现顺序性&#xff0c;以及生产者和消费者应该如何配合。   首先&#xff0c;顺序写入。Kafka的消息是按分区追加写入…

Node.js技术原理分析系列——Node.js调试能力分析

本文由体验技术团队屈金雄原创。 Node.js 是一个开源的、跨平台的 JavaScript 运行时环境&#xff0c;它允许开发者在服务器端运行 JavaScript 代码。Node.js 是基于 Chrome V8引擎构建的&#xff0c;专为高性能、高并发的网络应用而设计&#xff0c;广泛应用于构建服务器端应…

从技术债务到架构升级,滴滴国际化外卖的变革

背 景 商家营销简述 在外卖平台的运营中&#xff0c;我们致力于通过灵活的补贴策略激励商家&#xff0c;与商家共同打造良好的合作关系&#xff0c;也会提供多样化的营销活动&#xff0c;帮助商家吸引更多用户下单。通过这些活动&#xff0c;不仅能够提高商家的销量&#xff0c…

SQL Query美化

推荐一个可以美化Query的网站&#xff01; 名称&#xff1a;SQL formatter | Online free SQL Beautifier 地址&#xff1a;https://sqlformatter.org/# 在处理 SQL 查询语句时&#xff0c;可读性是至关重要的。 杂乱无章的 SQL代码不仅难以理解&#xff0c;还会给后续的维…

2025 docker可视化管理面板DPanel的安装

1.什么是 DPanel &#xff1f; DPanel 是一款 Docker 可视化管理面板&#xff0c;旨在简化 Docker 容器、镜像和文件的管理。它提供了一系列功能&#xff0c;使用户能够更轻松地管理和部署 Docker 环境。 软件特点&#xff1a; 可视化管理&#xff1a;提供直观的用户界面&#…

mapbox V3 新特性,添加下雪效果

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;mapbox 从入门到精通 文章目录 一、&#x1f340;前言1.1 ☘️mapboxgl.Map 地图对象…

【STM32】H743的以太网MAC控制器的一个特殊功能

调试743的MAC&#xff0c;翻阅手册的时候&#xff0c;发现了一个有意思的功能 混杂模式 H743的MAC控制器&#xff0c;可以设置为混杂模式&#xff0c;这就意味着它可以做一些网络监控的应用&#xff0c;譬如连接具备端口镜像功能的交换机&#xff0c;然后直接代替PC实现网络数据…

WEB攻防-第60天:PHP反序列化POP链构造魔术方法流程漏洞触发条件属性修改

目录 一、序列化与反序列化基础 1.1 什么是序列化与反序列化 二、魔术方法的生命周期 2.1 常见的魔术方法 2.2 模式方法的生命周期触发调用 2.2.1 __construct() 2.2.2 __destruct() 2.2.3 __sleep() 2.2.4 __wakeup() 2.2.5 __invoke() 2.2.6 __toS…

二、交换机的vlan子设备接入

一、交换机的vlan设置-CSDN博客 二、交换机的vlan子设备接入-CSDN博客 接上篇的文章&#xff0c;本文接入了子设备 网络结构如下&#xff1a; 用路由器A和POE交换机B代替第一篇中的笔记本电脑&#xff0c;路由器A和交换机B都关闭DHCP服务&#xff0c;并分别接入一个IPC&#…

Spring IoC的实现机制是什么?

大家好&#xff0c;我是锋哥。今天分享关于【Spring IoC的实现机制是什么&#xff1f;】面试题。希望对大家有帮助&#xff1b; Spring IoC的实现机制是什么&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring IoC&#xff08;Inversion of Control…