【Linux】套接字编程

这篇具有很好参考价值的文章主要介绍了【Linux】套接字编程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

套接字

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 位整数由当前主机序列转化为网络序列。

【Linux】套接字编程,网络,Linux,linux,运维,服务器

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 函数,其用于创建套接字打开网络文件。

【Linux】套接字编程,网络,Linux,linux,运维,服务器

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

【Linux】套接字编程,网络,Linux,linux,运维,服务器

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

【Linux】套接字编程,网络,Linux,linux,运维,服务器

🌸最后一个参数默认为 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 函数。

【Linux】套接字编程,网络,Linux,linux,运维,服务器

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

sockaddr 结构

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

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

【Linux】套接字编程,网络,Linux,linux,运维,服务器

🌸接下来我们便需要对 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端口

【Linux】套接字编程,网络,Linux,linux,运维,服务器

🌸同样的,为了读取的数据接下来使用,在读取时要给 \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 这个函数,最后两个函数代表我们要将这个消息发送给谁。

【Linux】套接字编程,网络,Linux,linux,运维,服务器

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;
    }
}

运行效果

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

【Linux】套接字编程,网络,Linux,linux,运维,服务器

拓展 

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

🌸好了,我们今天的内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。文章来源地址https://www.toymoban.com/news/detail-751609.html

到了这里,关于【Linux】套接字编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux网络编程(二-套接字)

    目录 一、背景知识 1.1 端口号 1.2 网络字节序 1.3 地址转换函数  二、Socket简介 三、套接字相关的函数  3.1 socket() 3.2 bind() 3.3 connect() 3.4 listen() 3.5 accept()  3.6 read()/recv()/recvfrom() 3.7 send()/sendto()  3.8 close()  四、UPD客服/服务端实验  1.1 端口号 端口号是访问服务器的标识 ,就好像

    2024年01月22日
    浏览(92)
  • 【Linux】网络---->套接字编程(TCP)

    TCP的编程流程:大致可以分为五个过程,分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。 1.准备过程:服务端和客户端需要创建各自的套接字,除此之外服务端还需要绑定自己的地址信息和进行监听。注意:服务端调用listen函数后,处理监听状

    2024年02月04日
    浏览(67)
  • 【Linux】网络编程套接字一

    上篇博客由唐僧的例子我们知道: 在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。 思考一下: 不考虑中间的一系列步骤,两台主机我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上。 但是我们把

    2024年03月26日
    浏览(256)
  • 【Linux网络】网络编程套接字(TCP)

    目录 地址转换函数 字符串IP转整数IP 整数IP转字符串IP 关于inet_ntoa 简单的单执行流TCP网络程序 TCP socket API 详解及封装TCP socket  服务端创建套接字  服务端绑定  服务端监听  服务端获取连接  服务端处理请求 客户端创建套接字 客户端连接服务器 客户端发起请求 服务器测试

    2024年03月21日
    浏览(71)
  • linux【网络编程】之网络套接字预备

    在【网络基础】中我们提到了IP地址,接下来了解一下网络通信中其他方面的知识 端口号是一个2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; 一个端口号只能被一个进程占用 通信原理 (公网)IP唯一标识一台主机,这样两台

    2024年02月05日
    浏览(91)
  • day-08 基于Linux的网络编程(套接字和标准I/O、分离I/O流、epoll、多线程服务器)

    标准I/O函数(stdio)是在C语言中用于进行输入和输出操作的库函数 。它们包括了一组标准的输入和输出函数,如printf、scanf、fopen、fclose等。标准I/O函数具有以下优点: 简单易用 :标准I/O函数提供了简洁的接口,使得输入和输出操作变得简单易用。开发人员无需自行处理底层

    2024年02月09日
    浏览(62)
  • Linux网络编程——tcp套接字

    本章Gitee仓库:tcp套接字 客户端: 客户端: 关于构造和初始化,可以直接在构造的时候,将服务器初始化,那为什么还要写到 init 初始化函数里面呢? 构造尽量简单一点,不要做一些“有风险”的操作。 tcp 是面向连接的,通信之前要建立连接,服务器处于等待连接到来的

    2024年02月20日
    浏览(58)
  • 【Linux】网络基础+UDP网络套接字编程

    只做自己喜欢做的事情,不被社会和时代裹挟着前进,是一件很奢侈的事。 1. 首先计算机是人类设计出来提高生产力的工具,而人类的文明绵延至今一定离不开人类之间互相的协作,既然人类需要协作以完成更为复杂的工作和难题,所以计算机作为人类的工具自然也一定需要

    2024年02月08日
    浏览(63)
  • 【Linux网络】网络编程套接字(预备知识+UDP)

    目录 预备知识 1. 理解源IP地址和目的IP地址 2. 理解源MAC地址和目的MAC地址 3. 认识端口号  4. 理解源端口号和目的端口号 5. 端口号(port) vs 进程pid 6. 认识TCP协议和认识UDP协议 7. 网络字节序 socket编程接口  1. socket 常见API 2. sockaddr结构  简单的UDP网络程序  1. 服务端创建udp

    2024年02月19日
    浏览(58)
  • Linux网络编程- 原始套接字(Raw Socket)

    原始套接字(Raw Socket)提供了一种机制,允许应用程序直接访问底层传输协议,绕过操作系统提供的传输层接口。这种套接字通常用于实现新的协议或对现有协议进行低级别的操作。 以下是对原始套接字的详细介绍: 定义与用途 : 原始套接字是直接基于网络层(如IP)的。

    2024年02月07日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包