文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

【Linux网络】网络编程套接字 -- 基于socket实现一个简单UDP网络程序

2023-09-01 17:59

关注

我们把数据从A主机发送到B主机,是目的吗?不是,真正通信的不是这两个机器!其实是这两台机器上面的软件(人)

数据有IP(公网)标识一台唯一的主机,用谁来标识各自主机上客户或者服务进程的唯一性呢?
为了更好的表示一台主机上服务进程的唯一性,我们采用端口号port标识服务器进程,客户端进程的唯一性!

端口号(port)是传输层协议的内容:

ip地址(主机全网唯一性) + 该主机上的端口号,标识该服务器上进程的唯一性
IP保证全网唯一,port保证在主机内部的唯一性

主机上对应的服务进程,在全网中是唯一的一个进程。
网络通信的本质:其实就是进程间通信

  1. 需要让不同的进程,先看到同一份资源—网络
  2. 通信就是在做IO,所以我们所有的上网行为,无外乎两种:我要把我的数据发出去、我要收到别人给我发的数据

进程已经有pid,为什么要有port呢?

  1. 系统是系统,网络是网络,单独设置—系统与网络解耦
  2. 需要客户端每次都能找到服务器进程—服务器的唯一性不能做任何改变— IP+port不能随意改变不会轻易改变。
  3. 不是所有的进程都要提供网络服务或者请求,但是所有的进程都需要pid。

进程+port–>网络服务进程
底层OS如何根据port找到指定的进程:OS内部采用hash方案,在OS内部维护了一个基于端口号的哈希表,key就是端口号,value就是task_struct的地址。有这个端口号就可以找到PCB,继而找到文件描述符表,文件描述符对象,对象找到了那么这个文件的缓冲区也就能找到,然后就可以将数据拷贝到缓冲区,最后就相当于我们将网络数据放到了文件中,如同读文件一样就将数据读上去了。

一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;

理解源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号目的端口号。 就是在描述 “数据是谁发的,要发给谁”;

认识TCP(Transmission Control Protocol 传输控制协议)协议:

认识UDP(User Datagram Protocol 用户数据报协议)协议:


我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  3. 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  4. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  5. 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可;

在这里插入图片描述

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

在这里插入图片描述

处理字节序函数 htonl、htons、ntohl、ntohs

在这里插入图片描述
函数原型:

uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);

函数作用:
将数据在不同字节序之间进行转换。

函数的详细介绍:


socket编程接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP, 服务器)int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

按道理要实现上述三种套接字应该要三套不同的接口,但是设计者只设计了一套接口,通过不同的参数解决所有网络或其他场景下的通信问题

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同
在这里插入图片描述

sockaddr 结构:

struct sockaddr  {    __SOCKADDR_COMMON (sa_);    char sa_data[14];  };

sockaddr_in 结构:

struct sockaddr_in  {    __SOCKADDR_COMMON (sin_);    in_port_t sin_port;    struct in_addr sin_addr;        unsigned char sin_zero[sizeof (struct sockaddr) -   __SOCKADDR_COMMON_SIZE -   sizeof (in_port_t) -   sizeof (struct in_addr)];  };

虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型、端口号、IP地址。

in_addr结构:

typedef uint32_t in_addr_t;struct in_addr  {    in_addr_t s_addr;  };

in_addr用来表示一个IPv4IP地址。其实就是一个32位的整数。

显示当前户籍UDP连接状况与端口号的使用情况:

sudo netstat -nuap

结尾实现UDP程序的socket接口使用解析

socket

int socket(int domain, int type, int protocol);

函数作用:
用于创建一个新的网络套接字的系统调用。

函数参数:
domain 参数指定了网络协议族:

type 参数指定了套接字的类型

protocol 参数指定了使用的协议

使用 socket() 函数的一般流程如下:

  1. 创建一个套接字:调用 socket() 函数,指定 domain、type 和 protocol 参数,返回一个新的套接字描述符。
  2. 绑定套接字到本地地址:调用 bind() 函数,将套接字和一个本地地址绑定,以便其他程序可以通过该地址找到该套接字。
  3. 监听连接请求:如果创建的是面向连接的流套接字,可以调用 listen() 函数,开始监听连接请求。
  4. 接受连接请求:如果创建的是面向连接的流套接字,可以调用 accept() 函数,接受一个连接请求,返回一个新的套接字描述符,用于与客户端通信。
  5. 发送和接收数据:调用 send() 和 recv() 函数,向对方发送数据或接收对方发送的数据。
  6. 关闭套接字:调用 close() 函数,关闭套接字描述符,释放相关资源。

处理 IP 地址的函数

在这里插入图片描述

int inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in);struct in_addr inet_makeaddr(int net, int host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);

初始化sockaddr_in

sockaddr_in是一个 IPv4 地址结构体,用于存储 IP 地址和端口号信息。在使用套接字函数时,通常需要将地址信息存储在 sockaddr_in 结构体中,并将其作为参数传递给函数

struct sockaddr_in local; // 定义了一个变量,栈,用户    bzero(&local, sizeof(local));//用于将指定的内存区域清零    local.sin_family = AF_INET;//将结构体成员 sin_family 设置为 AF_INET,表示使用 IPv4 地址族    local.sin_port = htons(_port);     //结构体成员 sin_port 设置为要使用的端口号,使用 htons() 函数将端口号转换为网络字节序(大端字节序)        local.sin_addr.s_addr = inet_addr(_ip.c_str());    //将结构体成员 sin_addr 设置为要使用的 IP 地址,使用 inet_addr() 函数将 IP 地址转换为网络字节序(大端字节序)    //在实际开发中,可以使用 inet_pton() 函数将字符串形式的 IP 地址转换为一个 struct in_addr 类型的结构体,该结构体包含了 IP 地址的二进制表示

bind

int bind(int socket, const struct sockaddr *address,              socklen_t address_len);

函数作用:
用于将一个本地地址(IP 地址和端口号)与一个套接字关联起来的函数

函数参数:

函数返回值:
返回值为 0 表示绑定成功,-1 表示绑定失败,错误码保存在 errno 变量中。

recvfrom

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,              int flags, struct sockaddr *restrict address,              socklen_t *restrict address_len);

函数作用:
用于从已连接或未连接的套接字接收数据的函数

函数参数:

函数返回值:
函数返回值为接收到的数据的字节数,如果没有数据可用,则返回 0。如果发生错误,则返回 -1,错误码保存在 errno 变量中

sendto

ssize_t sendto(int socket, const void *message, size_t length,              int flags, const struct sockaddr *dest_addr,              socklen_t dest_len);

函数作用:
用于向已连接或未连接的套接字发送数据的函数

函数参数:

函数返回值:
函数返回值为发送数据的字节数,如果发生错误,则返回 -1,错误码保存在 errno 变量中。


封装服务器相关代码

udpServer.hpp

#pragma once#include #include #include #include #include #include #include #include #include #include #include #include namespace Server{    using namespace std;    static const string defaultIp = "0.0.0.0"; //TODO    static const int gnum = 1024;    enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};    class udpServer    {    public:        udpServer(const uint16_t &port, const string &ip = defaultIp)        :_port(port), _ip(ip), _sockfd(-1)        {}        void initServer()        {            // 1. 创建socket            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);            if(_sockfd == -1)            {                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;                exit(SOCKET_ERR);            }            cout << "socket success: " << " : " << _sockfd << endl;            // 2. 绑定port,ip(TODO)            // 未来服务器要明确的port,不能随意改变            struct sockaddr_in local; // 定义了一个变量,栈,用户            bzero(&local, sizeof(local));            local.sin_family = AF_INET;            local.sin_port = htons(_port);            local.sin_addr.s_addr = inet_addr(_ip.c_str());            //local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法            int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));            if(n == -1)            {                cerr << "bind error: " << errno << " : " << strerror(errno) << endl;                exit(BIND_ERR);            }            // UDP Server 的预备工作完成        }        void start()        {            // 服务器的本质其实就是一个死循环            char buffer[gnum];            for(;;)            {                // 读取数据                struct sockaddr_in peer;                socklen_t len = sizeof(peer); //必填                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);                // 1. 数据是什么 2. 谁发的?                if(s > 0)                {                    buffer[s] = 0;                    string clientip = inet_ntoa(peer.sin_addr); //1. 网络序列 2. int->点分十进制IP                    uint16_t clientport = ntohs(peer.sin_port);                    string message = buffer;                    cout << clientip <<"[" << clientport << "]# " << message << endl;                }            }        }        ~udpServer()        {        }    private:        uint16_t _port;        string _ip; // 实际上,一款网络服务器,不建议指明一个IP        int _sockfd;        // func_t _callback; //回调    };}

udpServer.cc

#include "udpServer.hpp"#include using namespace std;using namespace Server;static void Usage(string proc){    cout << "\nUsage:\n\t" << proc << " local_port\n\n";}// ./udpServer portint main(int argc, char *argv[]){    if(argc != 2)    {        Usage(argv[0]);        exit(USAGE_ERR);    }    uint16_t port = atoi(argv[1]);    std::unique_ptr<udpServer> usvr(new udpServer(port));    usvr->initServer();    usvr->start();    return 0;}

封装客户端相关代码

udpClient.hpp

#pragma once#include #include #include #include #include #include #include #include #include #include #include namespace Client{    using namespace std;    class udpClient    {    public:        udpClient(const string &serverip, const uint16_t &serverport)         : _serverip(serverip),_serverport(serverport), _sockfd(-1), _quit(false)        {}        void initClient()        {            // 创建socket            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);            if (_sockfd == -1)            {                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;                exit(2);            }            cout << "socket success: " << " : " << _sockfd << endl;            // 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要            // 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind        }        void run()        {            struct sockaddr_in server;            memset(&server, 0, sizeof(server));            server.sin_family = AF_INET;            server.sin_addr.s_addr = inet_addr(_serverip.c_str());            server.sin_port = htons(_serverport);            string message;            while(!_quit)            {                cout << "Please Enter# ";                cin >> message;                sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));            }        }        ~udpClient()        {        }    private:        int _sockfd;        string _serverip;        uint16_t _serverport;        bool _quit;    };} // namespace Client

udpClient.cc

#include "udpClient.hpp"#include using namespace Client;static void Usage(string proc){    cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";}// ./udpClient server_ip server_portint main(int argc, char *argv[]){    if(argc != 3)    {        Usage(argv[0]);        exit(1);    }    string serverip = argv[1];    uint16_t serverport = atoi(argv[2]);    unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));    ucli->initClient();    ucli->run();    return 0;}

实验结果

在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

来源地址:https://blog.csdn.net/DEXTERFUTIAN/article/details/131986759

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-人工智能
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯