【Linux】套接字编程

news2025/2/5 12:36:52

目录

套接字

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.coloradmin.cn/o/1232280.html

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

相关文章

11.4MyBatis(基础)

一.搭环境 1.创建完SSM项目,添加MySQL和MyBatis后,项目启动一定会报错,这是正常情况. 2.配置文件 properties: server.port9090 spring.datasource.urljdbc:mysql://127.0.0.1:3306/test1?characterEncodingutf8&useSSLfalse spring.datasource.usernameroot spring.d…

Linux内核的安装

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

开发仿抖音APP遇到的问题和解决方案

uni-app如何引入阿里矢量库图标/uniapp 中引入 iconfont 文件报错文件查找失败 uni-app如何引入阿里矢量库图标 - 知乎 uniapp 中引入 iconfont 文件报错文件查找失败&#xff1a;‘./iconfont.woff?t1673007495384‘ at App.vue:6_宝马金鞍901的博客-CSDN博客 将课件中的cs…

企业微信将应用安装到工作台

在上篇中介绍了配置小程序应用及指令、数据回调获取第三方凭证&#xff1b; 本篇将介绍如何将应用安装到企业工作台。 添加测试企业 通过【应用管理】->【测试企业配置】添加测试企业。 通过企业微信扫描二维码添加测试企业。 注意&#xff1a;需要扫描的账号为管理员权限…

让别人访问电脑本地

查看本地IP地址&#xff1a; 使用ipconfig&#xff08;Windows&#xff09;或ifconfig&#xff08;Linux/macOS&#xff09;命令来查看你的计算机本地网络的IP地址。确保*****是你的本地IP地址。 防火墙设置&#xff1a; 确保你的防火墙允许从外部访问*****。你可能需要在防火…

万字解析设计模式之代理模式

一、代理模式 1.1概述 代理模式是一种结构型设计模式&#xff0c;它允许通过创建代理对象来控制对其他对象的访问。这种模式可以增加一些额外的逻辑来控制对原始对象的访问&#xff0c;同时还可以提供更加灵活的访问方式。 代理模式分为静态代理和动态代理两种。静态代理是在编…

【机器学习】032_多种神经网络层类型

一、密集层 每一层神经元都是上一层神经元的函数&#xff0c;每层每个神经元都从前一层获得所有激活的输入。 整个神经网络前一层与后一层连接在一起&#xff0c;构造的网络密集。 二、卷积层 假设有一张大小为axb像素的图片&#xff0c;上面标着一些手写数字&#xff0c…

Apache Airflow (十二) :PythonOperator

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

Java-equals方法

1.package com.msb.test02; 2. 3./** 4. * Auther: msb-zhaoss 5. */ 6.public class Phone {//手机类&#xff1a; 7. //属性&#xff1a; 8. private String brand;//品牌型号 9. private double price;//价格 10. private int year ;//出产年份 11. //方法&a…

Java基础-----正则表达式

文章目录 1.简介2.目的3.学习网站4.常用匹配字符5.String类中用到正则表达式的方法 1.简介 又叫做规则表达式。是一种文本模式&#xff0c;包括普通字符和特殊字符&#xff08;元字符&#xff09;。正则使用单个字符来描述、匹配一系列某个句法规则的字符串&#xff0c;通常用…

投资黄金:如何选择正确的黄金品种增加收益?

黄金一直以来都是备受投资者青睐的避险资产&#xff0c;然而&#xff0c;在庞大的黄金市场中&#xff0c;选择适合自己的黄金品种成为影响收益的关键因素。黄金投资并不只有一种方式&#xff0c;而是有很多种不同的黄金品种可以选择。每种黄金品种都有其独特的特点和风险&#…

Linux本地WBO创作白板部署与远程访问

文章目录 前言1. 部署WBO白板2. 本地访问WBO白板3. Linux 安装cpolar4. 配置WBO公网访问地址5. 公网远程访问WBO白板6. 固定WBO白板公网地址 前言 WBO在线协作白板是一个自由和开源的在线协作白板&#xff0c;允许多个用户同时在一个虚拟的大型白板上画图。该白板对所有线上用…

启动dubbo消费端过程提示No provider available for the service的问题定位与解决

文/朱季谦 某次在启动dubbo消费端时&#xff0c;发现无法从zookeeper注册中心获取到所依赖的消费者API&#xff0c;启动日志一直出现这样的异常提示 Failed to check the status of the service com.fte.zhu.api.testService. No provider available for the service com.fte…

使用Python的turtle模块绘制玫瑰花图案(含详细Python代码与注释)

1.1引言 turtle模块是Python的标准库之一&#xff0c;它提供了一个绘图板&#xff0c;让我们可以在屏幕上绘制各种图形。通过使用turtle&#xff0c;我们可以创建花朵、叶子、复杂的图案等等。本博客将介绍如何使用turtle模块实现绘制图形的过程&#xff0c;并展示最终结果。 …

初始环境配置

目录 一、JDK1、简介2、配置步骤 二、Redis1、简介2、配置步骤 三、MySQL1、简介2、配置步骤 四、Git1、简介2、配置步骤 五、NodeJS1、简介2、配置步骤 六、Maven1、简介2、配置步骤 七、Tomcat1、简介2、配置步骤 一、JDK 1、简介 JDK 是 Oracle 提供的 Java 开发工具包&…

Java基础-----StringBuffer和StringBuilder

文章目录 1.StringBuffer1.1 构造方法1.2 常用方法 2.StringBuilder3.String、StringBuffer、StringBuilder的区别 1.StringBuffer 内容可变的字符串类&#xff0c;适应StringBuffer来对字符串的内容进行动态操作&#xff0c;不会产生额外的对象。StringBuffer在初始时&#x…

机器学习笔记 - Ocr识别中的CTC算法原理概述

一、文字识别 在文本检测步骤中,分割出了文本区域。现在需要识别这些片段中存在哪些文本。 机器学习笔记 - Ocr识别中的文本检测EAST网络概述-CSDN博客文章浏览阅读300次。在 EAST 网络的这个分支中,它合并了 VGG16 网络不同层的特征输出。现在,该层之后的特征大小将等于 p…

【计算机网络笔记】路由算法之链路状态路由算法

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

Redis高级特性和应用(发布 订阅、Stream)

目录 发布和订阅 操作命令 发布消息 订阅消息 查询订阅情况 查看活跃的频道 查看频道订阅数 使用场景和缺点 Redis Stream Stream总述 常用操作命令 生产端 消费端 单消费者 消费组 创建消费组 消息消费 在Redis中实现消息队列 基于pub/sub 基于Stream Re…

18章总结—Swing程序设计

例题1 package admi; import java.awt.*; import javax.swing.*; public class JFreamTest { public static void main(String[] args) { JFrame jfnew JFrame(); jf.setTitle("创建一个JFrame窗体"); Container containerjf.getC…