【Linux】套接字编程

2023/11/30 9:16:33

目录

套接字

IP + PORT

TCP和UDP的介绍

TCP

UDP

网络字节序

转换接口

UDP服务器的编写

服务器的初始化 

socket

bind

sockaddr 结构

服务器的运行

数据的收发

业务处理

客户端的编写

运行效果

拓展 


套接字

🌸首先,我们先思考一个问题,数据从 A 主机发送到 B 主机是网络通信的最终目的吗

🌸显然不是的,我们进行网络通信是为了二者能通过某种协同方式,共同完成一个任务。因此,数据传输到 B 主机的传输层后并不能就此结束,还要向上交付给应用层。

🌸同时,我们还应该注意到,客户端与服务端本质上都是运行起来的服务,即二者都是正在运行的进程

🌸因此,网络通信的本质是进程间通信。

IP + PORT

🌸所以在网络通信的过程中必定经历这两个步骤。

  • 先将数据通过OS,将数据发送到目标主机。
  • 再将本主机收到的数据,推送给自己上层的指定进程。

🌸我们知道通过 IP 地址定位一台主机,而在网络中我们使用 port端口号(2字节)来定位主机上的进程。

🌸这时候我们突然想起来,之前在系统中不是使用了 pid 作为进程的唯一标识符吗?那这里为什么不继续使用 pid 标识进程呢?

🌸我们需要知道的是,pid 是属于操作系统部分的概念,若直接在网络中使用 pid 则会增加操作系统和网络直接的耦合度,当数据需要修改时则牵一发而动全身。

🌸因此,使用 IP + PORT就可以定位到互联网中的唯一进程,即网络通信的本质是通过 IP 和 PORT 构建进程的唯一性,基于网络的进程间通信。

🌸而通过IP和PORT来标志进程唯一性的方案就叫做套接字通信(socket)。

TCP和UDP的介绍

🌸在传输层我们有两个十分常见的传输协议,分别是 TCP UDP 协议,下面就分别介绍一下二者的区别。

TCP

🌸TCP是一种有连接可靠传输面向字节流的一种传输协议。

🌸面向字节流就好比家里的自来水,你要用多少就接收多少,而还未使用的数据就以流的形式存放在缓冲区中。

🌸而可靠传输体现在 TCP 需要保证对方收到对应消息,若未收到就会进行重发。

UDP

🌸UDP则是无连接不可靠传输面向数据报的传输协议。

🌸面向数据报的形式就像是我们收快递那样,一次至少接收一个完整的快递,不能收半个快递。

🌸需要注意的是,可靠性是一个中性词,并没有谁好谁坏,因为 UDP 在传输过程中并不关心对方是狗收到对应的报文,所以传输的过程中若丢失了对应的数据报就是真的丢失了。

🌸TCP 保证可靠性自然需要做更多的工作来维护,因而使用成本较高,而 UDP 并不保证,因此使用起来比较简单,二者并无谁优谁劣

网络字节序

🌸我们都知道多字节的数据在内存中存放具有大小端之分,不同主机间的存储方式也不同,那么在网络通信过程中该如何解决这个问题呢?

🌸TCP/IP 协议规定,网络数据流统一采用大端字节序,因此若当前发送的主机是小端机就需要先将数据转成大端,再进行通信。

转换接口

🌸对于整数的转换,有以下的接口,函数的名字和作用很好记,h 代表 host 即当前主机,n 代表 net 即网络序列,后面的 s16 位整数,l32 位整数。如 htons 就是将 16 位整数由当前主机序列转化为网络序列。

UDP服务器的编写

🌸接下来我们一起来学习一下 UDP 服务器是如何编写的吧。

🌸为了方便管理,这里直接将服务器封装起来了,对于一个服务器对象需要将其初始化,接着才能让它运行起来,因此一开始的类中,便需要以下几个成员函数。

namespace Alpaca
{
    class UdpServer
    {
    public:
        UdpServer()
        {}
        ~UdpServer()
        {}
        void InitServer()
        {}
        void Start()
        {}
    };
}

🌸而在主函数中,只需要创建一个服务器的对象就能让他运行起来了。

int main(int argc, char *argv[])
{
    unique_ptr<UdpServer> usvr(new UdpServer());

    usvr->InitServer();
    usvr->Start();
    return 0;
}

服务器的初始化 

socket

🌸首先介绍的便是 socket 函数,其用于创建套接字打开网络文件。

🌸其中第一个参数用于选择通信的协议族,有以下几种可以选择,一般我们进行网络通信填 AF_INET 即可。

🌸其二的 type 参数用于指定通信语义,还记得我们上面讲的 TCP 是面向字节流的一种协议,而 UDP 则是面向数据报的一种协议,因此若是使用 TCP 协议直接使用 SOCK_STREAM 即可,而使用 UDP 则填入 SOCK_DGRAM

🌸最后一个参数默认为 0 即可。

🌸而 socket 函数的返回值是一个文件描述符,就像我们在文件系统那样,需要先存起来,之后还会用到。

_sock = socket(AF_INET, SOCK_DGRAM, 0);    //用成员先存起来

🌸因为返回的值表示为文件描述符,所以当返回值 < 0 时则说明创建套接字失败,便不能进行接下来的操作,直接结束进程。

if (_sock < 0)
{
    std::cout << "create socket error: " << strerror(errno) << std::endl;
    exit(SOCKET_ERR);
}

🌸这里返回的错误码另外定义就行,这里我是使用枚举来定义的。

enum
{
    USAGR_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

🌸如此,我们便成功创建了套接字。 

bind

🌸成功创建套接字后,我们需要将套接字与 IP 和端口进行绑定,使用的便是 bind 函数。

🌸第一个参数自然就是先前创建的文件描述符,而第二个参数则需要接下来着重介绍了。

sockaddr 结构

🌸sockaddr 这个结构就类似于使用C语言的方法简单实现了一个多态的处理方式,当头部的地址类型为 AF_INET 就以 struct addr_in 的方式解析结构体,若是 AF_UNIX 则使用 struct addr_un 的方式解析。

🌸而 AF_INETAF_UNIX 在上方 socket 函数就介绍过了,即我们需要进行网络通信时则填充 struct addr_in,而要本地通信则填充 struct addr_un,强转后传给 bind 函数即可。

🌸接下来我们便需要对 sockaddr_in 结构体进行填充,需要注意的是,这里填入数据需要以网络字节序的形式,同时我们也有对应的接口协助我们进行转换,端口的转换使用 htons ,而 IP 则可以使用 inet_adddr 进行转换。

🌸但由于这里我使用的是云服务器,因此并不需要绑定 IP 地址,因此这里填入的便是 INADDR_ANY,若是使用虚拟机便需要绑定对应的 IP 地址。

struct sockaddr_in local;
bzero(&local, sizeof local);    //清空操作可选可不选
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;

if (bind(_sock, (sockaddr*)&local, sizeof(local)))    //传参时需要强转
{
    std::cout << "bind socket error: " << strerror(errno) << std::endl;
    exit(BIND_ERR);
}

🌸同时,绑定的端口我们可以自己默认设置,或者使用命令行参数进行传入。

🌸这里我将从命令行参数里面提取对应的端口,然后通过构造函数构造出对应的服务器对象。

void usage(string proc)    //使用提示
{
    cout << "Usage:\n\t" << proc << " port" << endl;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGR_ERR);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr<UdpServer> usvr(new UdpServer(port));

    usvr->InitServer();
    usvr->Start();
    return 0;
}

🌸由此,我们便完成了 UDP 网络通信的前期准备,接下来只要根据业务运行服务器即可。 

服务器的运行

🌸完成了服务器的初始化,接下来就是服务器运行函数的实现了。下面我们就简单地实现一个收发操作吧。

数据的收发

🌸对于数据的接收,我们使用的是 recvfrom 这个函数,使用的情况与文件操作中的读取操作并无太大区别,但值得注意的是后面两个参数为输入输出型参数,用于接收发送者的相关信息,我们便能够从中提取出对应的 IP端口

🌸同样的,为了读取的数据接下来使用,在读取时要给 \0 留一个位置,读取完对应的内容再加上,同时,我们服务器提供的服务是时刻运行的,因此还需要持续不断的循环。

void Start()
{
    char buffer[1024];
    while (true)
    {
        // 接收信息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);
        if (n > 0)
            buffer[n] = '\0';
        else
            continue;
    }
}

🌸而发送信息则使用 sendto 这个函数,最后两个函数代表我们要将这个消息发送给谁。

sendto(_sock, buffer, strlen(buffer), 0, (sockaddr *)&peer, sizeof(peer));

🌸我们便可以将接收到的数据打印出来,再发回给客户端,完成一个简单的交互。

🌸而客户端的相关数据就存在返回回来的 sockaddr_in 结构体中,经过转换就能够直接输出了。

void Start()
{
    char buffer[1024];
    while (true)
    {
        // 接收信息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);
        if (n > 0)
            buffer[n] = '\0';
        else
            continue;

        // 提取客户端信息
        std::string clientip = inet_ntoa(peer.sin_addr);
        uint16_t port = ntohs(peer.sin_port);
        std::cout << "get message from " << clientip << "-" << port << ": " << buffer << std::endl;

        // 发送信息
        sendto(_sock, buffer, strlen(buffer), 0, (sockaddr*)&peer, sizeof(peer));
    }
}

业务处理

🌸客户端向服务器发送数据,一定需要服务器提供某种服务,自然不是简单的收发操作。

🌸因此,我们再外部定义一个函数,将其作为服务器类中的一个成员,当需要使用服务时就回调对应的函数。

🌸为了方便,我们事先使用了包装器,对函数类型进行包装。

using func_t = std::function<std::string(std::string)>;

🌸这里我们可以简单写一个服务用于将小写字符转成大写。 

std::string transString(std::string request)
{
    for (auto& c : request)
    {
        if (islower(c))
            c = toupper(c);
    }
    return request;
}

🌸接着在构造的时候传入类中即可。

unique_ptr<UdpServer> usvr(new UdpServer(transString, port));

🌸最后,当我们收到对应的数据时,先对其进行处理,经过服务后再发回客户端

void Start()
{
    char buffer[1024];
    while (true)
    {
        // 接收信息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);
        if (n > 0)
            buffer[n] = '\0';
        else
            continue;

        // 提取客户端信息
        std::string clientip = inet_ntoa(peer.sin_addr);
        uint16_t port = ntohs(peer.sin_port);
        std::cout << "get message from " << clientip << "-" << port << ": " << buffer << std::endl;

        std::string resp = _service(buffer);

        // 发送信息
        sendto(_sock, resp.c_str(), resp.size(), 0, (sockaddr*)&peer, sizeof(peer));
    }
}

客户端的编写

🌸完成了服务器的编写,客户端的流程也有异曲同工之处,由于服务器已经封装过了,这里的客户端我们便直接在主函数中写了。

🌸首先确定的一点便是,客户端一定是知道服务器 IP 和端口号的,因此我们可以在客户端启动的时候从命令行里获取。

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

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        return USAGR_ERR;
    }

    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
}

🌸接着,便是开始网络通信的前期准备,即创建套接字,和上面讲过的方式并无差别,这里就直接跳过了。

int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
    std::cerr << "create socket error: " << strerror(errno) << std::endl;
    exit(SOCKET_ERR);
}

🌸值得注意的一点来了,虽然客户端也要进行 bind ,但并不需要我们自己 bind ,也不要自己 bind操作系统会自动给我们 bind

🌸主要原因是,如果明确写死了端口号便可能与其他客户端的端口发生冲突,因此由 OS 为客户端分配空闲的端口。

🌸既然不用绑定端口,那么接下来我们就可以进行数据发送的准备工作了,在上面因为我们是直接拿接收下来的发送方的信息作为对象发送数据。

🌸这里需要先将服务端的信息填充进结构体。

sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());

🌸接着便能进入循环的服务中,我们可以通过命令行获取要发送的信息,再使用一个缓冲区接收服务端发送回的数据,当收到数据时就将对应的数据输出即可。

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

    sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&server, sizeof(server));

    char buffer[1024];
    struct sockaddr_in tmp;
    socklen_t len = sizeof(tmp);
    int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&tmp, &len);
    if (n > 0)
    {
        buffer[n] = '\0';
        cout << "server echo# " << buffer << std::endl;
    }
}

运行效果

🌸将服务器和客户端运行起来,尝试向服务器发送信息,便成功收到其回应,同时当我们输入的信息有小写的字符时便会将其转换成大写。

拓展 

🌸好了,这下我们搭建的服务器已经初具雏形了,接着可以往几个方向进行拓展,比如增加多线程的模块,分配一个线程专门进行数据的接收,而另一个线程则进行数据的发送。同时,可以将发送过信息的 IP 与端口加入注册表中,用 hash 进行维护,一旦接收到信息就向其中所有用户进行广播,便可以构成一个小型的聊天组。

🌸好了,我们今天的内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。


http://www.jnnr.cn/a/1081980.html

相关文章

EI论文程序:Adaboost-BP神经网络的回归预测算法,可作为深度学习对比预测模型,丰富实验内容,自带数据集,直接运行!

适用平台&#xff1a;Matlab 2021及以上 本程序参考中文EI期刊《基于Adaboost的BP神经网络改进算法在短期风速预测中的应用》&#xff0c;程序注释清晰&#xff0c;干货满满&#xff0c;下面对文章和程序做简要介绍。 为了提高短期风速预测的准确性&#xff0c;论文提出了使用…

队列的实现和OJ练习

目录 概念 队列的实现 利用结构体存放队列结构 为什么单链表不使用这种方法&#xff1f; 初始化队列 小提示&#xff1a; 队尾入队列 队头出队列 获取队头元素 获取队尾元素 获取队列中有效元素个数 检测队列是否为空 销毁队列 最终代码 循环队列 队列的OJ题 …

十. Linux关机重启命令与Vim编辑的使用

关机重启命令 shutdown命令 其他关机命令 其他重启命令 系统运行级别 系统默认运行级别与查询 退出登录命令logout 文本编辑器Vim Vim简介 没有菜单,只有命令Vim工作模式 Vim常用命令 插入命令 定位命令 删除命令 复制和剪切命令 替换和取消命令 搜索和搜索替换命令 保存和退出…

微机原理_14

一、单项选择题(本大题共15小题,每小题3分,共45分。在每小题给出的四个备选项中,选出一个正确的答案。&#xff09; 1,下面寻址方式的操作数不在存储器中的是(&#xff09; A. 堆栈寻址 B. 寄存器间址 C.寄存器寻址 D. 直接寻址 2,条件转移指令JNE的条件是(&#xff09; A. CF…

【每日刷题——语音信号篇】

思考与练习 练习2.1 语音信号在产生的过程中&#xff0c;以及被感知的过程中&#xff0c;分别要经过人体的哪些器官&#xff1f; 1.产生过程&#xff1a; 肺部空气 → \rightarrow →冲击声带 → \rightarrow →通过声道&#xff08;可以调节&#xff09; → \rightarrow →…

【洛谷 P3743】kotori的设备 题解(二分答案+循环)

kotori的设备 题目背景 kotori 有 n n n 个可同时使用的设备。 题目描述 第 i i i 个设备每秒消耗 a i a_i ai​ 个单位能量。能量的使用是连续的&#xff0c;也就是说能量不是某时刻突然消耗的&#xff0c;而是匀速消耗。也就是说&#xff0c;对于任意实数&#xff0c;…

ModernCSS.dev - 来自微软前端工程师的 CSS 高级教程,讲解如何用新的 CSS 语法来解决旧的问题

今天给大家安利一套现代 CSS 的教程&#xff0c;以前写网页的问题&#xff0c;现在都可以用新的写法来解决了。 ModernCSS.dev 是一个现代 CSS 语法的教程&#xff0c;讲解新的 CSS 语法如何解决一些传统问题&#xff0c;一共有30多课。 这套教程的作者是 Stephanie Eckles&am…

人工智能给我们的生活带来了巨大的影响?

1. 人工智能从哪些方面给我们带来了影响&#xff1f; 人工智能出现&#xff0c;极大地影响了人类的生活&#xff0c;下面是人工智能所影响的领域&#xff1a; 1. 日常生活 智能家居: AI驱动的设备&#xff0c;如智能扬声器、灯光、恒温器&#xff0c;正在改变我们与家居环境的…

基于AVR单片机的便携式心电监测设备设计与实现

基于AVR单片机的便携式心电监测设备是一种常用的医疗设备&#xff0c;用于随时监测和记录人体的心电信号。本文将介绍便携式心电监测设备的设计原理和实现步骤&#xff0c;并提供相应的代码示例。 1. 设计概述 便携式心电监测设备是一种小巧、方便携带的设备&#xff0c;能够…

基于水基湍流算法优化概率神经网络PNN的分类预测 - 附代码

基于水基湍流算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于水基湍流算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于水基湍流优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

Linux内核的安装

1.通过tftp 加载内核和根文件系统 即sd内存卡启动&#xff1a; SD卡的存储以扇区为单位,每个扇区的大小为512Byte, 其中零扇区存储分区表&#xff08;即分区信息&#xff09;,后续的扇区可自行分区和格式化&#xff1b; 若选择SD卡启动&#xff0c;处理器上电后从第一个扇区开…

【C++上层应用】2. 预处理器

文章目录 【 1. #define 预处理 】【 2. #ifdef、#if 条件编译 】2.1 #ifdef2.2 #if2.3 实例 【 3. # 和 ## 预处理 】3.1 # 替换预处理3.2 ## 连接预处理 【 4. 预定义宏 】 预处理器是一些指令&#xff0c;指示编译器在实际编译之前所需完成的预处理。 所有的预处理器指令都是…

Node.js环境配置级安装vue-cli脚手架

一、下载安装Node.js (略) 二、验证node.js并配置 1、下载安装后&#xff0c;cmd面板输入node -v查询版本、npm -v ,查看npm是否安装成功&#xff08;有版本号就行了&#xff09; 2、选择npm镜像&#xff08;npm config set registry https://registry.npm.taobao.org&…

AIGC ChatGPT4对Gbase数据库进行总结

ChatGPT4 用一个Prompt完成Gbase数据库的总结。 AIGC ChatGPT 职场案例 AI 绘画 与 短视频制作 PowerBI 商业智能 68集 数据库Mysql 8.0 54集 数据库Oracle 21C 142集 Office 2021实战应用 Python 数据分析实战&#xff0c; ETL Informatica 数据仓库案例实战 Excel 2021实操 …

【CHI】Ordering保序

本节介绍CHI协议所包含的支持系统保序需求的机制&#xff0c;包括&#xff1a; • Multi-copy atomicity • Completion response and ordering • Completion acknowledgment • Transaction ordering 一、 Multi-copy atomicity CHI协议中所使用的memory model要求为mu…

ClickHouse的 MaterializeMySQL引擎

1 概述 MySQL 的用户群体很大&#xff0c;为了能够增强数据的实时性&#xff0c;很多解决方案会利用 binlog 将数据写入到 ClickHouse。为了能够监听 binlog 事件&#xff0c;我们需要用到类似 canal 这样的第三方中间件&#xff0c;这无疑增加了系统的复杂度。 ClickHouse 20.…

11.16~11.19绘制图表,导入EXCEL中数据,进行拟合

这个错误通常是由于传递给curve_fit函数的数据类型不正确引起的。根据你提供的代码和错误信息&#xff0c;有几个可能的原因&#xff1a; 数据类型错误&#xff1a;请确保ce_data、lg_data和product_data是NumPy数组或类似的可迭代对象&#xff0c;且其元素的数据类型为浮点数。…

【grafana | clickhouse】实现展示多折线图

说明&#xff1a; 采用的是 Visualizations 的 Time series&#xff0c;使用的 clickhouse 数据源 在工作中遇到了一个需求&#xff0c;写好了代码&#xff0c;需要在grafana上展示在一个项目中所有人的&#xff0c;随时间的代码提交量变化图 目前遇到的问题&#xff1a;展示…

使用树莓派学习Linux系统编程的 --- 库编程(面试重点)

在之前的Linux系统编程中&#xff0c;学习了文件的打开&#xff1b;关闭&#xff1b;读写&#xff1b;进程&#xff1b;线程等概念.... 本节补充“Linux库概念 & 相关编程”&#xff0c;这是一个面试的重点&#xff01; 分文件编程 在之前的学习中&#xff0c;面对较大的…

音视频同步笔记 - 以音频时间为基

音视频同步 - 以音频时间为基 上图介绍&#xff1a; 该图是以音频的时间为基&#xff0c;对视频播放时间的延迟控制方案&#xff0c;只调整视频的播放延时。delayTime是视频播放的延迟时间&#xff0c;初始值是1 / FPS * 1000 (ms)&#xff0c;如果FPS为25帧率&#xff0c;初始…
最新文章