前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux】Socket编程—TCP

【Linux】Socket编程—TCP

作者头像
大耳朵土土垚
发布于 2025-02-15 02:32:14
发布于 2025-02-15 02:32:14
58700
代码可运行
举报
文章被收录于专栏:c/c++c/c++
运行总次数:0
代码可运行

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服务器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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服务。

多进程版本
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//其他的不变
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;
    }

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

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

多线程版本
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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;
    }

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

线程池版本
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
    }

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

使用服务器代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include "TcpServer.hpp"

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

TCP客户端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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连接后才能通信。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Mysql必知必会!
数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以对数据库中的数据进行增加,修改,删除及查询操作
网络技术联盟站
2020/09/29
2K0
Mysql必知必会!
MySQL表的增删查改
values左侧为表中属性,右侧为自定义插入的内容,左右两侧安装顺序是一一对应的,如果顺序不同就会导致类型不同而出错。
每天都要进步呀
2023/10/16
6410
MySQL表的增删查改
mysql必备语句
1,没有数据库,使用磁盘文件存储数据; 2, 层次结构模型数据库; 3,网状结构模型数据库; 4,关系结构模型数据库:使用二维表格来存储数据; 5,关系-对象模型数据库; MySQL就是关系型数据库!
cherishspring
2019/10/14
13K0
MySQL学习笔记汇总(一)——简单查询、条件查询、数据排序。
今天的分享就到这里啦!!~感谢大家的观看,希望对大家有帮助的话麻烦给个丝滑三连击。(点赞+转发+关注) 一起加油,一起努力,一起秃见成效!!
百思不得小赵
2022/12/01
1.3K0
MySQL学习笔记汇总(一)——简单查询、条件查询、数据排序。
数据库select语句详解
1)检索单个列 select ename from emp; 2) 检索多个列 select ename,job,sal from emp; 3) 检索所有列 select * from emp; 4) 去除重复 select distinct deptno from emp; 5) 别名 select ename as 姓名 from emp; 6) 伪列,即不存在的列,构建虚拟的列 select empno, 1*2 as count,‘cmj’ as name,deptno from emp; 7)虚表,及不存在的表,可以计算 select 1+1 from dual;
全栈程序员站长
2022/09/06
2.5K0
数据库select语句详解
Oracle应用实战五——SQL查询
Oracle SQL SQL学习是重点,请仔细阅读。 O Oracle 结构化查询语言(Structured Query Language)简称SQL(发音:/ˈɛs kjuː ˈɛl/ "S-Q
Java帮帮
2018/03/19
1.4K0
Oracle应用实战五——SQL查询
Oracle数据库学习笔记 (四 —— select 从入门到放弃 【上】)
基本语法 order by xxxx asc(desc) asc 升序, desc 降序
Gorit
2021/12/09
1.2K0
Oracle 中的SELECT 关键字(查询、检索)
检索单个列:select 列名 from 表名; 例:select ename from emp; 检索多个列: select [列1,列2, ... ,列N] from 表名; 例:select ename , sal from emp; 检索所有列:select * from 表名; 例:select * from emp;
星哥玩云
2022/08/18
4.6K0
oracle--单表查询
---单表的查询学习 --查询表的所有数据 select * from 表名;*代表所有 select * from emp; --查询表中指定字段的值 select 字段名1,字段名2,...from表名 select empno from emp; select empno,ename from emp; --给查询结果中的字段使用别名 --在字段名后使用关键字 字段名 as "别名" --作用:方便查看查询结果 --注意:as关键字可以省略不写,别名中没有特殊字符双引号也可以省略不写。 select empno 员工编号,ename"员工 姓名",job as 工作,mgr as "领导编号" from emp; --连接符:select 字段名||'字符'||字段名||..... from 表名 --||为sql语句的字符链接符,使用在select和from之间 --字符链接格式为 字段名||'字符'||字段名 --注意:一个拼接好的连接在结果集中是作为一个新的字段显示,可以使用别名优化字段显示。 select empno||'的姓名是'||ename as"信息",job||'哈哈'||mgr from emp; --去除重复 select distinct 字段名,字段名,...fromn 表名 ---注意:去除重复的规则是按照行进行去除的,多行数据完全相同取其一 select distinct job ,mgr from emp; --排序 --单字段排序 --select * from 表名 order by 字段名 asc 升序排序 asc可以省略不写 --select * from 表名 order by 字段名 desc 降序序排序 --多字段排序 --select * from emp order by 字段名1,字段名2... --先按照字段1排序,如果字段1的值相同,则按照字段2排序,.... select * from emp order by empno desc--单字段排序 降序 select empno,ename,job from emp order by ename asc--单字段排序 升序 select * from emp order by empno,ename--多字段排序 --字段的逻辑运算 --select关键字和from关键字之间的字段可以直接进行四则运算 --字段与字段之间也可以直接进行运算 --注意:字段值为数值类型 select * from emp select empno,ename,job,sal*2+1000,sal+comm from emp ----------------------------------------------------------------- --使用where子句查询筛选 --select 字段名,字段名,...from表名 where 筛选条件 --单筛选条件 --使用运算符进行筛选 =,>,>=,<,<=,<> 单个条件中 --注意:如果条件中的值为字符,必须使用单引号括起来 --查询所有的员工的工资信息 select empno,ename,sal+comm as 薪资 from emp --查询SMITH的个人信息 select * from emp where ename='SMITH' --查询SMITH的薪资信息,逻辑运算符= select empno,ename,sal,sal+comm from emp where ename='SMITH' --查询工资大于1000的员工信息,逻辑符> select * from emp where sal>'2000' --查询工资不等于3000的员工信息 select * from emp where sal<>3000 order by sal --练习: --查看工资等于1250的员工信息
eadela
2019/09/29
7660
MySQL查询语句
  select * from emp;  在日常工作中 不建议使用* 因为查询效率较低
用户7630333
2023/12/07
1.1K0
MySQL查询语句
Oracle数据库之限定查询和排序显示详解
范例:根据之前的查询结果发现 SMITH 的工资最低,现在希望可以取得 SMITH 的详细资料。
星哥玩云
2022/08/18
1.4K0
PLSQL 基础教程 三 查询(SELECT)
本节教程将继续介绍SQL基础知识中的SELECT相关的一些知识,包括基础语法、多表连接、去重、排序、子查询等等SELECT方面的基础知识。
全栈程序员站长
2022/08/31
4.6K0
PLSQL 基础教程 三 查询(SELECT)
常用sql查询语句
5.9 合并查询(union 并集, intersect 交集, union all 并集+交集, minus差集)
FGGIT
2024/10/15
3130
《数据库查询:解锁数据宝藏的魔法之钥》
MySQL查询是一种用于检索、筛选和分析数据的数据库操作技术。作为一个强大的关系型数据库管理系统(RDBMS),MySQL支持多种查询方法,包括使用SQL(Structured Query Language)编写的查询语句。
杨不易呀
2023/09/27
2520
《数据库查询:解锁数据宝藏的魔法之钥》
MySQL表的增删改查
引言:CRUD 即增加(Create)、查询(Retrieve)、更新(Update)、删除(Delete)四个单词的首字母缩写
用户11305962
2024/10/09
3840
MySQL表的增删改查
mysql系列一
学习mysql必备工具即安装mysql客户端;mysql安装教程在网上有很多,在此处就不在仔细说明;
沁溪源
2020/09/03
1.1K0
MySQL入门学习笔记(上)
英文单词DataBase,简称DB。按照一定格式存储数据的一些文件的组合。 顾名思义:存储数据的仓库,实际上就是一堆文件。这些文件中存储了具有特定格式的数据。
啵啵鱼
2022/11/23
1.9K0
MySQL入门学习笔记(上)
Oracle数据库 sql条件查询语句与练习
a)、= 、 >、 <、 >=、 <=、 !=、 <>、 between and b)、and 、or、 not、 union、 union all、 intersect 、minus c)、null :is null、 is not null、 not is null d)、like :模糊查询 % _ escape('单个字符') f)、in 、 exists(难点) 及子查询m
wolf
2020/09/20
1.2K0
推荐学Java——数据表操作
上节内容学习了数据库 MySQL 的安装、验证、数据库管理工具、数据库的基本操作命令,还没有学习的同学可以从主页去看上一篇推送内容。
逆锋起笔
2022/01/13
2.8K0
推荐学Java——数据表操作
mysql入门
定义:操作一组数据(多行记录)返回一个结果,也叫分组函数 大多用于统计 例如:统计各部门中雇员的人数。统计各部门中最高和最低薪资
崔笑颜
2020/06/08
1.1K0
相关推荐
Mysql必知必会!
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 1. TCP socket API 详解
    • socket()
    • bind()
    • listen()
    • accept()
    • connect()
  • 2. Echo Server
    • TCP服务器
      • 多进程版本
      • 多线程版本
      • 线程池版本
    • TCP客户端
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档