Linux网络编程 - socket套接字 - UDP协议

IP地址(公网IP),标定了主机的唯一性。

端口号,标识特定主机上的网络进程的唯一性。

因此IP+端口号port,就能标识全网唯一的一个进程。

IP+端口号 = 套接字。称之为套接字编程。

(利用套接字进行网络通信,本质上也是进程间通信的,只是这两个进程不在同一个主机上,要想进行通信,必须通过网络)


TCP:传输控制协议,传输层协议的一种。有链接。可靠传输。面向字节流。

UDP:用户数据报协议,传输层协议的一种。无连接。不可靠传输。面向数据报。

udp_server.hpp

#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_

#include "log.hpp"
#include <string>
#include <cstring>

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 基于UDP协议的服务端
class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip), _sock(-1)
    {
    }
    void initServer()
    {
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 1.套接字类型:网络套接字(不同主机间通信) 2.面向数据报还是面向字节流:UDP面向数据报
        // SOCK_DGRAM支持数据报(固定最大长度的无连接、不可靠消息)。
        if (_sock < 0)
        {
            // 创建套接字失败?
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2.bind : 将用户设置的ip和port在内核中和我们当前的进程强关联
        struct sockaddr_in local; // 传给bind的第二个参数,存储ip和port的信息。
        local.sin_family = AF_INET;
        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!-> 故需要转换为网络字节序
        local.sin_port = htons(_port); // host->network l 16
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域,4字节
        // INADDR_ANY:让服务器在工作过程中,可以从本机的任意IP中获取数据(一个服务器可能不止一个ip,(这块有些模糊)
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        // 点分十进制字符串风格的IP地址 <-> 4字节整数   4字节主机序列 <-> 网络序列   inet_addr可完成上述工作
        if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0) // !!!
        {
            logMessage(FATAL, "bind : %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server %s...", strerror(errno));
    }
    void start()
    {
        // 作为一款网络服务器,永远不退出的!-> 服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        char buff_message[1024]; // 存储从client发来的数据
        char back_message[124];
        std::string back_str;
        for (;;)
        {
            struct sockaddr_in peer; // 输出型参数
            memset(&peer, 0, sizeof peer);
            socklen_t len = sizeof peer; // 输出+输入型参数

            // 读取client发来的数据
            ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len);  // 哪个ip/port给你发的
            // receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
            if (sz > 0)
            {
                // 从client获取到了数据
                buff_message[sz] = 0;
                // 你发过来的字符串是指令 ls -a -l, rm -rf ~
                if(strcasestr(buff_message, "rm") != nullptr)
                {
                    std::string err_msg = "可恶..";
                    std::cout << inet_ntoa(peer.sin_addr)<< " : "<< ntohs(peer.sin_port)  << err_msg << buff_message << std::endl;
                    sendto(_sock, err_msg.c_str(), err_msg.size(), 0, (struct sockaddr*)&peer, len);
                    continue;
                }
                FILE* fp = popen(buff_message, "r");
                if(fp == nullptr)
                {
                    logMessage(ERROR, "popen : %d:%s\n", errno, strerror(errno));
                    continue;
                }
                while(fgets(back_message, sizeof(back_message), fp) != nullptr)
                {
                    back_str += back_message;
                }
                fclose(fp);
            }
            else
            {
                // back_str.clear();
            }
            // 作为一款伪shell server,任务就是写回client发来的命令的结果,结果存储在back_str中
            sendto(_sock, back_str.c_str(), back_str.size(), 0, (struct sockaddr *)&peer, len);
            back_str.clear();
        }
    }
    void start1()
    {
        // 作为一款网络服务器,永远不退出的!-> 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        char buff_message[1024]; // 存储从client发来的数据
        for (;;)
        {
            struct sockaddr_in peer; // 输出型参数
            memset(&peer, 0, sizeof peer);
            socklen_t len = sizeof peer; // 输出+输入型参数

            // 读取client发来的数据
            ssize_t sz = recvfrom(_sock, buff_message, sizeof(buff_message) - 1, 0, (struct sockaddr *)&peer, &len);  // 哪个ip/port给你发的
            // receive a message from a socket,从一个套接字(或许对应网卡)中接收信息
            if (sz > 0)
            {
                // 从client获取到了非空数据
                buff_message[sz] = 0;
                uint16_t cli_port = ntohs(peer.sin_port);
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节网络序列ip->字符串风格的IP
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buff_message);    // 可有可无...服务端显示一下客户端发来了什么
            }
            else
            {
                buff_message[0] = 0;
            }
            // 作为一款echo server,任务就是写回client发来的数据
            sendto(_sock, buff_message, strlen(buff_message), 0, (struct sockaddr *)&peer, len);
        }
    }
    ~UdpServer()
    {
        close(_sock);
    }
private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;  // 端口号
    std::string _ip; // ip
    int _sock;   // 套接字
};

#endif

udp_server.cc

#include "udp_server.hpp"
#include <iostream>
#include <memory>
#include <cstdlib>

static void Usage(const char *proc)
{
    std::cout << "\nUsage: " << proc << " port\n"
              << std::endl;
}

// 格式:./udp_server 8080
// 疑问: 为什么不需要传ip?
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

udp_client.cc

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void Usage(char *proc)
{
    std::cout << "\nUsage : " << proc << " server_ip server_port\n"
              << std::endl;
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0); // 网络套接字,udp面向数据报
    if(sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    // client要不要bind??要,但是一般client不会显式地bind,程序员不会自己bind
    // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
    // 故client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?见下方)

    std::string message;     // 用户输入数据的缓冲区

    struct sockaddr_in server;      
    memset(&server, 0, sizeof server);
    server.sin_port = htons(atoi(argv[2]));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char message_svr[1024];
    memset(message_svr, 0, sizeof message_svr);
    while (true)
    {
        std::cout << "client# ";
        std::getline(std::cin, message);
        if(message == "quit") break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);   // server:给哪个ip/port发

        struct sockaddr_in temp;
        memset(&temp, 0, sizeof temp);
        socklen_t len = sizeof temp;
        ssize_t sz = recvfrom(sock, message_svr, sizeof message_svr - 1, 0, (struct sockaddr *)&temp, &len);  // temp:谁发给你的(哪个ip/port)
        if(sz > 0)
        {
            message_svr[sz] = 0;
            std::cout << "server echo# "<< message_svr << std::endl;
        }
    }
    close(sock);
    return 0;
}

log.hpp略了。

UDP是面向数据报的传输层协议。

服务端基本套路就是,socket创建套接字。bind绑定端口和ip。recvfrom获取客户端发来的数据,进行业务处理(根据这个服务器的类型),sendto发送给客户端。

客户端:socket,不需要显式地bind(见注释),sendto给服务器,recvfrom获取服务器给你的回应数据。


套接字有三种,域间套接字,原始套接字,网络套接字。域间用于同一个主机内不同进程通信。原始略了。网络用于不同主机间进程进行数据通信。

这是三个场景,应当对应三套接口。但是不想设计过多接口,因此对于bind,recvfrom,sendto都有一个struct sockaddr*类型的参数。如果想使用网络套接字进行网络通信,就传struct sockaddr_in*类型然后强转。如果想使用域间套接字,就传struct sockaddr_un*类型。达到了一套接口,根据实参类型不同对应不同功能的目的。


上方示例其实就是一个udp套接字编程的基本使用,服务器也是一个最简单的不能再简单的echo server。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值