udp_socket

发布于:2024-12-09 ⋅ 阅读:(219) ⋅ 点赞:(0)

1. 常见API

 // 创建 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);

1.1 sockaddr 结构

未来在使用 socket 套接字编程时,一般默认是需要把本主机的IP地址与端口号通过系统调用接口进行绑定,其中的网络套接字就有不同的种类:

  • 域间套接字编程(用户一个主机内的多个进程间通信,即本地通信)
  • 原始套接字编程(绕过传输层,直接使用网络层、链路层的接口进行编码,通过用于编写网络工具:网络状态检测、网络抓包等)
  • 网络套接字编程(使用传输层进行用户通信)

在这里插入图片描述

由于网络通信场景的不同,因此引出了不同种类的套接字,理论上不同的套接字种类就需要不同的接口,但在网络接口的设计上,把网络接口全部统一抽象化,而网络接口统一的前提是,每一个接口的参数的类型必须是统一的。

实际上,我们在网络通信(网络套接字编程) 时使用的是 struct sockaddr_in 这个结构体,里面包含16位端口号、32位 IP 地址、8字节的填充字段,而在域间通信时使用的则是struct sockaddr _un,里面则只需要包含本机通信的两个进程看到同一份资源的路径,不同种类的网络套接字背后的数据类型是不同的。这两个结构体是作为编码时使用的数据类型,而在实际网络接口设计中,设计的是 struct sockaddr 这个接口,无论是网络通信 or 本地通信,前面都有 2 字节数据,表明通信的类型, struct sockaddr 同样也有这 2 个字节的数据,因此将来在使用网络接口时,传递的都是 struct sockaddr 结构体,然后在每个接口内部中都会实现分流,类似于:

if(address->type == AF_INEF) { 网络通信 }
else { 本地通信 }

这样一来,无论背后使用的是哪种通信(使用的是 struct sockaddr_in 或者 struct sockaddr _un),上层在调用网络接口时都不关心,在上层调用时统一使用 struct sockaddr,这就是所谓的 “将网络接口全部统一抽象化”,具体在后面编码实现时体现。


2. udp_socket

2.1 了解相关接口

NAME
      socket - create an endpoint for communication		// 创建套接字

SYNOPSIS
      #include <sys/types.h>          /* See NOTES */
      #include <sys/socket.h>

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

RETURN VALUE
	  成功时返回文件描述符。失败返回 -1,errno 被设置。后续的一切套接字的操作都需要通过返回的 socket 完成。
	  因此创建一个套接字的本质就是打开一个文件,struct file 指向网卡设备
      On  success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
		

参数:
domain:套接字的通信域(或协议族)(网络通信 or 本地通信),常见的包括:
	AF_INET:IPv4 地址族
	AF_INET6:IPv6 地址族
	AF_UNIX:本地通信(Unix 域套接字)
	AF_ROUTE:路由套接字
type:套接字的类型:
	SOCK_STREAM:面向流的套接字,提供可靠的、双向的字节流。
	SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的数据报通信。
	SOCK_RAW:原始套接字,允许直接访问底层协议(一般用于网络协议的研究和开发)。
protocol:指定特定的协议,默认设置为0,即系统自动选择与所选类型和域匹配的默认协议,也可指定特定协议,例如:
	对于 SOCK_STREAM,可以选择 IPPROTO_TCP(TCP 协议)。
	对于 SOCK_DGRAM,可以选择 IPPROTO_UDP(UDP 协议)。
// struct sockaddr_in 底层源码(部分)
typedef unsigned short int sa_family_t;
#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family		// ## 合并两边的符合
 
/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);	/* 底层宏替换##合并后即:sin_family  */
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
};
NAME
      bind - bind a name to a socket		// 绑定套接字

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

RETURN VALUE
      On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

参数:
sockfd:创建的套接字的返回值 
addr:上述介绍的 sockaddr_in (强转即可)
addrlen:addr 的大小
NAME
      recv, recvfrom, recvmsg - receive a message from a socket		// 接收数据报
       
      ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

RETURN VALUE
      These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno 
      is set to indicate the error. The return value will be 0 when the  peer has performed an orderly shutdown.

参数:
sockfd:创建的套接字的返回值 
buf:用于接收数据的缓冲区
len:缓冲区的大小
flags:指定接收数据的方式,默认设置为0,表阻塞接收
	MSG_WAITALL:在接收到所有请求的字节数之前不会返回,这使得函数在接收过程中会阻塞。
	MSG_PEEK:查看接收队列中的消息,但不从队列中移除任何消息,这样后续的接收操作仍然可以接收到这些消息。
	MSG_DONTWAIT:非阻塞模式;如果没有数据可接收,则立即返回,而不是阻塞等待。
addr:输出型参数,发送端的地址信息(如果接收到数据需要返回,必须知道对方的IP、Port等信息)
addrlen:addr 的大小
NAME
      send, sendto, sendmsg - send a message on a socket

      ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

RETURN VALUE
       On success, these  calls return the number of characters sent. On error, -1 is returned, and errno is set appropriately.
NAME
      popen, pclose - pipe stream to or from a process

SYNOPSIS
      #include <stdio.h>
		
	 // 底层调用 frok 创建子进程,并通过管道将指定命令传递给子进程执行,子进程的退出状态可通过pclose获取
      FILE *popen(const char *command, const char *type);

参数:
command:指定要执行的命令
type:指定打开管道的方式
  • 地址转换函数
    在这里插入图片描述
    本篇文章只介绍基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示32位的 IP 地址,但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换

     #include <arpa/inet.h>
    
     // 字符串 ==> in_addr
     int inet_aton(const char *cp, struct in_addr *inp);
    
     in_addr_t inet_addr(const char *cp);
    
     int inet_pton(int af, const char *src, void *dst);
    
     // in_addr ==> 字符串
     char *inet_ntoa(struct in_addr in);
     const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    

    其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr,因此函数接口是 void* addrptr。

    • 关于 inet_ntoa:

      inet_ntoa 这个函数返回了一个char*,即返回了字符串的起始地址,而字符串本身是在函数内部自己维护的(函数内部申请了一块内存来保存 ip 的结果),那么是否需要调用者手动释放呢?

      The inet_ntoa() function converts the Internet host address in, given in network byte order, to a string in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer, which subsequent calls will overwrite.

      man 手册对此进行了说明:inet_ntoa 函数是把转换后的 ip 字符串放到了静态存储区,这个时候不需要我们手动进行释放。那么问题来了,inet_ntoa把 结果放到自己内部的一个静态存储区,这样导致第二次调用时的结果会覆盖上一次的结果。

      在APUE中,明确提出 inet_ntoa 不是线程安全的函数,但是在 centos7 上测试并没有出现问题,因此可能是内部的实现加了互斥锁。因此后续推荐使用 inet_ntop,由用户自己提供缓冲区维护返回结果,来规避线程安全的隐患。

  • 关于 IP 的一个问题

    在这里插入图片描述
    云服务器禁止绑定公网 ip。因为有可能一些机器不只有一个网卡设备,那么就会配置多个 IP 地址。因此,这种配置的服务器下,如果只绑定了一个IP,那么数据向上交付时,只能收到发送给绑定IP的数据,数据发送给另外其它没有绑定的IP,这台服务器也收不到。所以,一般 bind 绑定 IP 地址时绑定的是 0,即凡是发给这台主机的数据,不管发送数据时绑定的是哪个IP,只要是这个主机的IP,都要根据端口号向上交付。

  • 关于 Port 的一个问题:

    在这里插入图片描述

    [0, 1023] 为系统内定的端口号,一般都要有固定的应用层协议使用,例如 http: 80、https: 443 等。

2.2 基础UDP通信

服务端承担数据的接收与转发,接收到的每条来自客户端的信息转发到已经启动的每一个客户端上。

客户端实现了多线程版本,将接收与发送分流执行。

// UdpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"

#define DEFAULT_PORT 8888
#define DEFAULT_IP "0.0.0.0"

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

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

Log log;
const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = DEFAULT_PORT, const std::string& ip = DEFAULT_IP)
        : _sockfd(0), _port(port), _ip(ip), _isRunning(false)
    {}

    void Init()
    {
        // 1. 创建 upd_socket
        // 2. Udp 的 socket 是全双工的,允许被同时读写的
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            log(Fatal, "socket create error, sockfd: %d", _sockfd);
            exit(SOCKET_ERR);
            exit(SOCKET_ERR);
        }
        log(Info, "socket create success, sockfd: %d", _sockfd);

        // 2. 绑定
        struct sockaddr_in local;
        bzero(&local, sizeof(local));   // 将指定空间全部初始化为0
        local.sin_family = AF_INET;     // 套接字结构体的前两个字节需要表明结构体的类型,也即通信的协议族:IPv4网络通信
        local.sin_port = htons(_port);  // 因为端口号是要通过网络发送给对方的,所以需要保证端口号是网络字节序
        // local.sin_addr.s_addr = inet_addr(_ip.c_str());    // 1. string->uint32_t  2. uint32_t 必须为网络字节序
        local.sin_addr.s_addr = INADDR_ANY;  

        int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));   // 网络接口统一设计的体现
        if(n == -1) 
        {
            log(Fatal, "bind error, errno: %d, err string: %s\n", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "socket bind success, errno: %d, err string: %s\n", errno, strerror(errno));
    }

    void CheckUser(const struct sockaddr_in client, const std::string clientip, uint16_t clientport)
    {
        auto iter = _online_user.find(clientip);
        if(iter == _online_user.end())
        {
            _online_user[clientip] = client;
            std::cout << "[" << clientip << ": " << clientport << "] add to online user." << std::endl;
        }
    }   
    void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport)
    {
        for(const auto& user : _online_user)
        {
            std::string message = "[" + clientip + ": " + std::to_string(clientport) + "]# " + info;
            sendto(_sockfd, message.c_str(), message.size(), 0, (const sockaddr*)(&user.second), (socklen_t)sizeof(user.second));
        }
    }

    // void Run(func_t func)
    void Run()
    {   
        _isRunning = true;
        char inbuffer[size];
        while (_isRunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log(Warning, "recvfrom error, errno: %d, err string: %s\n", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);      // 网络字节序 --> 本地
            CheckUser(client, clientip, clientport);

            inbuffer[n] = 0;
            std::string info = inbuffer;
            Broadcast(info, clientip, clientport);
        }
        
    }

    ~UdpServer()
    {
        if(_sockfd > 0) 
            close(_sockfd);
    }

private:
    int _sockfd; // 网络文件描述符
    std::string _ip;
    uint16_t _port;
    bool _isRunning;
    std::unordered_map<std::string, struct sockaddr_in> _online_user;
};
// UdpClient.cc
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

#include "Terminal.hpp"

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

struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;
};

void* recv_message(void* args)
{
    // OpenTerminal();      // 重定向终端

    ThreadData* td = static_cast<ThreadData*>(args);
    socklen_t len = sizeof(td->server);
    char buffer[4096];
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
    
}


void* send_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    socklen_t len = sizeof(td->server);
    string message;

    std::string welcome = "[" + td->serverip + "] is comming... ";
    sendto(td->sockfd, welcome.c_str(), welcome.size(), 0, (struct sockaddr*)&td->server, len);

    while(true)
    {
        std::cout << "Please Enter@ ";
        getline(cin, message);

        // cout << message << endl;
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&td->server, len);
    }
}


int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

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

    struct ThreadData td;
    bzero(&td.server, sizeof(td.server));
    td.serverip = serverip;
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(td.server);

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(td.sockfd < 0)
    {
        cout << "socket error" << endl;
        return 1;
    }

    // client 也需要 bind,因为 client 也有 ip、port,用于服务器返回的数据
    // 只不过 client 不需要用户显式绑定,一般由OS自由随机选择
    // 如果让用户自己绑定端口号,A进程绑定的是1234,B进程也是1234,那么哪个应用先启动哪个应用就能够正常使用,而后启动的进程就无法使用该端口了
    // 而不同企业的客户端也不可能协调使用客户端的端口号,因此客户端的端口号由OS自己选择最佳
    // 一个进程是可以绑定多个端口号的,如果让用户自己绑定,那么不妨有恶意进程绑定多个端口号,从而让其他进程无法使用
    // 对于服务器而言,端口号必须由用户自己绑定,则是因为服务器需要监听客户端的请求,请求都是由客户端向服务器发送的,服务器可从来不会主动发送给客户端
    // 而如果让服务器随机绑定端口号,那么可能每次启动时绑定的端口号都不一样,客户端下一次可能就无法请求服务器了。

    // 系统是在首次发送数据时,给客户端绑定端口号的。


    pthread_t receiver, sender;
    pthread_create(&receiver, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(receiver, nullptr);
    pthread_join(sender, nullptr);
    
    close(td.sockfd);
    return 0;
}
// Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024-65535]\n" << std::endl;
}

std::string handler(const std::string& str, const std::string clientip, uint16_t clientport)
{
    std::cout << "[" << clientip << ": " << clientport << "]# " << str;
    std::string res = "Server get a message: " + str;
    std::cout << res << std::endl;
    return res;
}

bool SafeCheck(const std::string& cmd)
{
    std::vector<std::string> key_words = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while",
        "touch",
        "for"
    };
    for(auto& word : key_words) 
    {
        auto pos = cmd.find(word);
        if(pos != std::string::npos) return false;
    }
    return true;
}

std::string ExcuteCommand(const std::string& cmd)
{
    std::cout << "get a request cmd: " << cmd << std::endl;
    if(!SafeCheck(cmd)) return "You are a bad-man!\n";

    FILE* fp = popen(cmd.c_str(), "r");
    if(fp == nullptr)
    {
        perror("popen");
        return "error";
    }
    std::string ret;
    char buffer[4096];
    while(true)
    {
        char* ok = fgets(buffer, sizeof(buffer), fp);
        if(ok == nullptr) break;
        ret += buffer;
    }

    pclose(fp);
    return ret;
}

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

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();

    return 0;
}

可以通过不同的终端,将客户端发送与接收的消息分离开,可以通过对输出流做重定向来完成。

 # 示例
 ./udpclient serverip serverport 2>/dev/pts/0

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!


网站公告

今日签到

点亮在社区的每一天
去签到