【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)

目录

TCP服务端

创建套接字

解决绑定失败问题

填充网络信息

绑定

设置监听状态

接受请求

收取和反馈消息

完整的服务端封装代码

TCP客户端

创建套接字

填充网络信息

发起连接

发送和收取消息

客户端完整代码

 一些补充


TCP服务端

初始化服务端

创建套接字

和UDP创建套接字的方式差不多,只不过我们要实现的是TCP第二个参数选用:SOCK_STREAM

解决绑定失败问题

服务端关闭绑定失败的问题通常出现在服务端程序退出后,重新启动时,可能会因为之前的套接字仍然处于 TIME_WAIT 状态,导致无法绑定到相同的地址和端口。解决这个问题的方法通常是通过设置 SO_REUSEADDRSO_REUSEPORT 选项。

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

填充网络信息

和UDP填充本地网络信息的方式一样,还是要注意以下几点:

  • 端口号主机序列转网络序列
  • IP地址点分十进制字符串转四字节网络序列
  • 设置任意的IP地址

绑定

和UDP绑定一样,要注意结构体的强转

设置监听状态

因为TCP要建立连接,一般是由客户端发起请求,服务端等待请求的到来。所以服务端要设置监听状态

int listen(int sockfd, int backlog);

返回值

listen() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即要进行监听的套接字。

  • backlog:是一个整数,指定在内核中等待处理的连接队列的最大长度。这个参数的具体含义是系统内核在拒绝新连接之前允许处于未连接状态(SYN_RCVD)的连接数量。

    如果队列满了,新的连接会被拒绝,并且客户端可能会收到 ECONNREFUSED 错误。较大的 backlog 值可以容纳更多的等待连接的客户端,但同时也会增加系统资源的消耗。

    如果 backlog 设置为 0,表示不接受连接队列,新连接会立即被拒绝。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

启动服务端

接受请求

accept() 函数用于接受客户端的连接请求,并创建一个新的套接字来处理该连接。这个新的套接字是与客户端建立的连接的专用套接字,通过它可以进行数据的收发。

返回值

accept() 函数的返回值是一个整数,表示新创建的套接字的文件描述符:

  • 如果成功,返回值是新创建的套接字的文件描述符。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

函数调用成功后的返回值是作为我们接下来进行读取数据的文件描述符,旧的套接字依旧作为监听套接字。 

参数

  • sockfd:是一个已经通过 socket() 函数创建并调用 bind() 函数绑定了地址的监听套接字描述符,即待处理连接的监听套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。这个参数是一个输出参数,accept() 函数会填写客户端的地址信息到这个结构体中。如果不需要知道客户端的地址,可以将这个参数设置为 NULL

  • addrlen:是一个指向 socklen_t 类型的指针,用于指定 addr 结构体的长度。在调用 accept() 函数之前,需要将 addrlen 设置为 addr 结构体的长度。这个参数也是一个输入输出参数,accept() 函数会将实际的客户端地址长度写入到这个变量中。

收取和反馈消息

因为建立连接后得到一个文件描述符,因此我们可以用使用文件的读写方法来进行我们的收取和反馈消息的操作。


完整的服务端封装代码

 TCPserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
const static int default_backlog = 5;
class TcpServer
{
public:
    TcpServer(uint16_t port)
        : _port(port), _isrunning(false)
    {
    }
    // 初始化服务器
    void Init()
    {
        // 第一步:套接字创建
        // 得到文件描述符
        // 本质是文件
        // 第一个参数表示域
        // 第二个参数表示套接字类型
        // 第三个参数为协议 默认缺省
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            cout << "Fatal Error" << endl
                 << "create socket error, errno code %d ,eror string %d",
                errno, strerror(errno);
            cout << endl;
            exit(2);
        }
        cout << "create socket success, sockfd : " << _listensock << endl;
        int opt=1;
        setsockopt(_listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
        //解决服务端关闭绑定失败问题
        // 第二步:填充本地网络信息并且绑定和监听
        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;
        // 设置内核空间 ——绑定
        if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) != 0)
        {
            cout << "bind Error" << "errno code : " << errno << "error string : " << strerror(errno) << endl;
            exit(3);
        }
        cout << "bind success" << endl;
        // Tcp需要建立连接,一般是由客户端发起请求 服务端一直等待连接的到来
        // Tcp需要监听
        // 设置套接字为监听状态
        if (listen(_listensock, default_backlog) != 0)
        {
            cout << "listen Error" << "errno code: " << errno << "error string : " << strerror(errno) << endl;
            exit(4);
        }
        cout << "listen success " << endl;
    }
    void Sverse(int fd)
    {
        char buffer[1024];
        while (1)
        {
            // 读取数据
            // tcp是面向字节流和文件管道差不多
            // 直接使用文件的读写方法即可

            ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                // 对同一个fd进行读写
                buffer[n] = 0;
                cout << "client say#" << buffer << endl;
                string echo_string = "client say#";
                echo_string += buffer;
                write(fd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0) // 如果返回值为0 ,代表读到了文件结尾 (对端关闭了连接)
            {
                cout << "client quit..." << endl;
                break;
            }
            else
            {
                cout << "read Error" << "error code : " << errno << "error string : " << strerror(errno) << endl;
                break;
            }
        }
    }
    void Start()
    {
        // 服务器启动了
        _isrunning = true;
        while (1)
        {
            // 先获取连接
            // 后两个参数为输入输出型参数//等同于UDP————recvfrom
            // 返回值 成功了返回一个非零的新的文件描述符(网络套接字) 失败了返回-1
            //
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                cout << "accept Error" << "error code: " << errno << "error string: " << strerror(errno) << endl;
                // 监听失败继续监听
                continue;
            }
            cout << "accept success , get a new sockfd : " << sockfd << endl;

            // 提供服务
            Sverse(sockfd);
            close(sockfd);
        }
    }

    ~TcpServer()
    {
        if (_listensock > 0)
        {
            close(_listensock);
        }
    }

private:
    // 端口号
    uint16_t _port;
    int _listensock;
    bool _isrunning;
};

Main.cc

#include<iostream>
#include<memory>
#include"TcpServer.hpp"
using namespace std;
void Usage(const string & process)
{
    cout<<"Usage:"<<endl<<process <<" local_port"<<endl;
}
int main(int argc ,char * argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }
    uint16_t port = stoi(argv[1]);
    auto* tsvr = new TcpServer(port);
    tsvr->Init();
    tsvr->Start();
    delete tsvr;
    return 0;
}

TCP客户端

创建套接字

和服务端的一样

填充网络信息

和服务端的一样

发起连接

connect() 函数用于客户端向服务器发起连接请求。它在客户端程序中使用,用于连接到远程服务器。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值

connect() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

 参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即待连接的套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储远程服务器的地址信息。通常是使用 struct sockaddr_instruct sockaddr_in6 结构体来表示 IPv4 或 IPv6 地址。这个参数包含了远程服务器的 IP 地址和端口号。

  • addrlen:是一个 socklen_t 类型的整数,表示 addr 结构体的长度。

注:这个参数调用成功操作系统会自动绑定。 

发送和收取消息

因为socket的本质也是一个文件描述符,因此我们可以像上面一样使用文件的读写方法来发送和收取消息。


客户端完整代码

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
bool visitserver(string &serverip, uint16_t serverport, int *cnt)
{
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        cout << "client sockfd error" << endl;
        return false;
    }
    bool ret = true;
    // 客户端不用绑定 , 客户端必须要有服务端的IP和port , 需要绑定,但是不需要用户显示绑定,因为client系统会随机绑定端口

    // Tcp发起连接的时候,client会被操作系统自动绑定

    // 发起连接
    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);
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); // 自动进行绑定
    // 连接失败
    if (n < 0)
    {
        cout << "connect error" << endl;
        ret = false;
        goto END;
    }
    // 重连成功,重连次数清零
    *cnt = 0;
    // 进行通信
    while (1)
    {
        string inbuffer;
        cout << "Plase Enter";
        getline(cin, inbuffer);
        if (inbuffer == "quit")
            break;
        ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());
        if (n > 0)
        {
            char buffer[1024];
            ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
            if (m > 0)
            {
                buffer[m] = 0;
                cout << "get a echo message ->" << buffer << endl;
            }
            else if (m == 0)
            {
                break;
            }
            else
            {
                ret = false;
                goto END;
            }
        }
        else
        {
            ret = false;
            goto END;
        }
    }
END:
    close(sockfd);
    return ret;
}
void Usage(const string &process)
{
    cout << "Usage : " << process << " server_ip server_port" << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);
    // 定制重连
    int cnt = 1;
    while (cnt <= 5)
    {
        bool result = visitserver(serverip, serverport, &cnt);
        if (result)
        {
            cnt = 5;
            break;
        }
        else
        {
            sleep(1);
            cout << "server offline, retrying...,cout : " << cnt++ << endl;
            cnt++;
        }
    }
    // 大于重连次数
    if (cnt >= 5)
    {
        cout << "server offline" << endl;
    }
    return 0;
}

//

一些补充

uint32_t inet_addr(const char *cp);

这个函数在上篇文章中是用来将点分十进制的字符串IP地址转化为四字节的网络序列的,但是这个函数由于它的实现方法原因是不是线程安全的函数,以后我们尽量使用下面的函数。

int inet_pton(int af, const char *src, void *dst);

返回值

  • 如果转换成功,返回值为 1。
  • 如果输入的地址格式不正确,返回值为 0。
  • 如果发生错误,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • af:指定地址族(Address Family),通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • src:指向以字符串形式表示的 IP 地址的指针,即点分十进制格式的 IP 地址。
  • dst:指向用于存储结果的缓冲区的指针,通常是一个 struct in_addr 结构体的指针(IPv4)或 struct in6_addr 结构体的指针(IPv6)。

今天对网络套接字的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!! 

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

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

相关文章

自然语言处理(NLP)原理、用法、案例、注意事项

自然语言处理&#xff08;Natural Language Processing&#xff0c;简称NLP&#xff09;是人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;领域的一个重要分支&#xff0c;旨在让计算机能够理解、理解和生成人类语言。 NLP的原理是基于统计建模和机…

list 的模拟实现

目录 1. list 的实现框架 2. push_back 3. 迭代器 4. constructor 4.1. default 4.2. fill 4.3. range 4.4. initializer list 5. insert 6. erase 7. clear 和 destructor 8. copy constructor 9. operator 10. const_iterator 10.1. 普通人的处理方案 10.2. …

物联网小demo

机智云生成代码 具体参考之前的文章 初始化 ADC用来使用光敏电阻 连续采样开启 采样的周期调高 定时器 定时器1用来实现延时 为了只用温湿度模块DHT11 定时器4用来和51进行交互 实现定时的发送和检测心跳信号 IIC 用来使用oled屏幕 USART 串口1和串口2是机智云自己…

Linux —— 信号初识

Linux —— 信号初识 什么是信号测试几个信号signal函数函数原型参数说明返回值注意事项示例 后台程序前台转后台检测输入中断向量表 我们今天来继续学习Linux的内容&#xff0c;今天我们要了解的是Linux操作系统中的信号&#xff1a; 什么是信号 信号是操作系统内核与进程之…

Hash Function(fft)

链接&#xff1a;H-Hash Function_2024牛客五一集训派对day4 (nowcoder.com) 题意&#xff1a;给定一个序列&#xff0c;求使得任意两数的hash值不同的最小模数&#xff1b; 分析&#xff1a;ab(mod seed) |a-b|%seed0; 也就是说seed不能是任意两数差的因子。 如果暴力求解…

【大麦小米学量化】使用Python读写通达信自选股(含代码转换及完整源代码),想要通过通达信自选股实现量化自动关联交易的有福了

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、通达信自选股文件所在位置二、通达信自选股文件数据结构三、使用Python读写通达信自选股文件&#xff08;附完整源代码&#xff09;1. 切换目录路径2. 将li…

4月30日重庆某厂酸碱管道整改工作汇报-智渍洁

时间:2024.4.30 地点:******老厂酸碱管道整改 施工人员:王成、汪勇、郭建华 事项:老厂酸碱管道更换 完成进度100%酸碱管道支架以添加完成&#xff01;碱管道保温已完成&#xff01; 1吨桶未完成2主水管漏水未处理&#xff0c;3酸 水泵需更换全新4室内少许添加活未完成。 4月30日…

精析React与Vue架构异同及React核心技术——涵盖JSX、组件、Props、State、生命周期与16.8版后Hooks深化解析

React&#xff0c;Facebook开源的JavaScript库&#xff0c;用于构建高性能用户界面。通过组件化开发&#xff0c;它使UI的构建、维护变得简单高效。利用虚拟DOM实现快速渲染更新&#xff0c;适用于单页应用、移动应用&#xff08;React Native&#xff09;。React极大推动了现代…

2-qt之信号与槽-简单实例讲解

前言、因实践课程讲解需求&#xff0c;简单介绍下qt的信号与槽。 一、了解信号与槽 怎样使用信号与槽&#xff1f; 概览 还记得 X-Window 上老旧的回调函数系统吗&#xff1f;通常它不是类型安全的并且很复杂。&#xff08;使用&#xff09;它&#xff08;会&#xff09;有很多…

Redis-分片机制

概述 业务需要&#xff1a;由于单台redis内存容量是有限的&#xff0c;无法实现海量的数据实现缓存存储 概念&#xff1a;由多个redis节点协助工作的机制就是redis的分片机制 作用&#xff1a;为了实现redis扩容 特点&#xff1a;分片机制把该机制中包含的多台redis缓存服务…

RK3568 学习笔记 : u-boot 下通过设置 env ethact 设置当前工作的以太网设备

前言 正点原子 &#xff1a;RK3568 开发板 atompi-ca1 默认有两个网口&#xff0c;通过 u-boot mii 命令&#xff0c;可以查看 网口信息 > mii device MII devices: ethernetfe010000 ethernetfe2a0000 Current device: ethernetfe010000u-boot 下的以太网&#xff0c;不同…

如何为 Nestjs 编写单元测试和 E2E 测试

前言 最近在给一个 nestjs 项目写单元测试&#xff08;Unit Testing&#xff09;和 e2e 测试&#xff08;End-to-End Testing&#xff0c;端到端测试&#xff0c;简称 e2e 测试&#xff09;&#xff0c;这是我第一次给后端项目写测试&#xff0c;发现和之前给前端项目写测试还…

UDP 的报文结构

一.UDP的报文结构 1.UDP的简单介绍 UDP是传输层协议&#xff0c;它是无连接,不可靠传输,面向数据报,全双工 1.无连接&#xff1a;UDP是一种无连接的传输协议&#xff0c;通信双方不需要在发送数据之前建立连接。相比之下&#xff0c;TCP是面向连接的协议&#xff0c;在传输数…

【除了协程还有哪些方式可以实现异步编程】

在Unity中&#xff0c;除了使用协程实现异步编程外&#xff0c;还有以下几种方法&#xff1a; 异步加载资源&#xff1a; 使用UnityWebRequest类进行异步加载资源&#xff0c;这在加载网络资源或动态加载资源时非常有用。 using UnityEngine; using UnityEngine.Networking;…

【Linux】进程控制 之 进程创建 进程终止 进程等待 进程替换

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…

每日一博 - 闲聊架构设计中的多级缓存设计

文章目录 方法论概述客户端缓存应用层缓存服务层缓存缓存设计的注意事项总结 思维导图戳这里 方法论概述 从客户端到服务层&#xff0c;缓存的应用广泛而重要。通过合理的缓存设计&#xff0c;能够有效地提高系统的性能并降低延迟。 客户端缓存 在客户端层面&#xff0c;浏览…

LLM2Vec介绍和将Llama 3转换为嵌入模型代码示例

嵌入模型是大型语言模型检索增强生成(RAG)的关键组成部分。它们对知识库和用户编写的查询进行编码。 使用与LLM相同领域的训练或微调的嵌入模型可以显著改进RAG系统。然而&#xff0c;寻找或训练这样的嵌入模型往往是一项困难的任务&#xff0c;因为领域内的数据通常是稀缺的。…

基于AT89C51单片机的温度上下限自动控制检报警设计

点击链接获取Keil源码与Project Backups仿真图: https://download.csdn.net/download/qq_64505944/89247694?spm=1001.2014.3001.5501 C 源码+仿真图+毕业设计+实物制作步骤+06 题 目 基于单片机的温度检测调节系统设计 姓 名 学 号 专业班级 指导教师 年 月 日 任务书 …

Nginx 从入门到实践(2)——Rewrite重写

Nginx Rewrite Rewrite重写 Nginx Rewriteurl组成说明Rewrite基本概述Rewrite使⽤场景rewrite优点 Rewrite配置语法location匹配概述 if指令if 判断指令语法nginx以及if 判断可使用的全局变量 set命令return指令 url组成说明 https://cn.bing.com/search?qNginxRewrite&P…

udp/tcp回显网络编程

udp DatagramSocket 用于接收和发送udp数据报 构造方法&#xff1a; DatagramSocket():创建一个UDP数据报套接字的Socket&#xff0c;绑定到本地上 一个随机可用端口上&#xff0c;一般用于客户端DatagramSocket(int port):创建一个UDP数据报套接字的Socket&#xff0c;绑定到…
最新文章