【计算机网络】手摸手带你写 UDP 回显程序、英译汉程序

本文涉及到的代码:https://github.com/ahojcn/code-linux/tree/master/LinuxCode/network/udp/practice

基础铺垫

首先来简单说几个问题。

1、程序双击后发生了什么?
程序双击后,程序会被操作系统装载入内存,包括程序的代码、数据等信息。然后从程序的入口开始执行。

2、本机的不同程序间可以相互对话(通信)吗?
当然是可以的,召唤师们经常用的 WeGame 和 LOL 是两个程序,当你在 WeGame 登录后点击启动 LOL,通常情况下再不用在 LOL 输入账号密码了,说明你再 WeGame 登录的账号信息也给了 LOL 的程序。至于是如何给的?
举个实现的例子:当你登录 WeGame 后,你会获得一个 ticket,这个 ticket 代表你的小票,这个小票就有着你的身份信息,你可以凭着这个小票进入召唤师峡谷。这个还得看程序员的具体的实现。

这个就属于进程间通信

3、如何实现不同主机间的不同进程的对话(通信)?
当然可以,你电脑上的 QQ 和室友电脑上的 QQ,双击后会被载入自己的内存,那么你发的话是如何被室友收到的呢?这个就是不同主机间不同进程的通信,利用网络通信!

4、那问题 3 中的两个 QQ 进程是如何找到对方的呢?
举个栗子:假设你有个女朋友,你的女朋友在北京上大学,你在西安上大学,你的女朋友给你准备了一份礼物,她要把这个礼物寄给你,她会填写快递信息如:
【中国陕西省西安市xxx区,西安xxx大学,xxx收,手机号xxx】
快递小哥可以根据这个地址和手机号唯一的定位到你,然后把快递送到你手里。

在计算机中也是有唯一标识的,这个东西就是 IP 地址,IP 地址标识了网络上的一台主机。
IP 地址就相当于【中国陕西省西安市xxx区,西安xxx大学】。
但是你的电脑上正在运行着很多程序,QQ、LOL、WeGame 等等,对方怎么知道这个包裹发给哪个程序呢?这个就需要另一个东西叫端口号。这个端口号就唯一的标识了你电脑上的一个程序。
端口号就相当于【xxx收,手机号xxx】。

这样就可以唯一的标识一个网络上一个主机的一个进程了。

5、什么是协议?
协议就是一种约定,用 4 中的栗子来说,你的女朋友给你寄的是特产烤鸭,她打电话给你说让你拿到后热一下再吃,这就是一种协议。
当然你也可以拿到后不热直接吃。(别吃坏肚子了)
当然她使用中文填写的快递地址信息也是和快递小哥的一种协议(约定)。

6、计算机有大端和小端之分,那数据在网络上传输的时候是按照什么方式传输呢?
规定网络传输的数据都以大端序传输,即先发出的数据是低地址,后发出的数据是高地址。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,下文的代码实现均会做网络字节序和主机字节序的转换。
转换需要用到的函数:

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);  // host to network long
uint16_t htons(uint16_t hostshort);  // host to network short
uint32_t ntohl(uint32_t netlong);  // network to host long
uint16_t ntohs(uint16_t netshort);  // network to host short

好了铺垫就到这里,来说说 UDP 协议。

UDP 协议

UDP 协议就是一种约定,约定了“快递小哥”拿到包裹后根据什么找到你。

UDP 协议格式如下:
在这里插入图片描述
其中需要注意的是:
1、2 个字节的 UDP 长度,是这整个 UDP 协议的长度,也就是说上面这张图的数据部分最多放 65535 - 64 字节的数据。资料:TCP、UDP数据包大小的限制
2、校验和,如果校验和出错(和实际收到的数据校验和对不上),这个包裹就被丢弃了。

UDP 网络回显程序

实现如下功能:
客户端 — hello —> 服务器
客户端 <— hello — 服务器

回显服务器测试

服务端实现

  1. 创建一个 socket;
  2. 服务器需要绑定一个固定的 IP 和端口号方便客户端找到他;
  3. 服务器启动并监听这个端口号;
  4. 循环处理:收到请求、处理请求、返回响应。
gcc echo_server.c -o echo_server
// echo_server.c
// UDP 回显服务器

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

int main() {
    // 创建 socket
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    // 绑定 IP 和 port
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    addr.sin_port = htons(8080);
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if (ret < 0) {
        perror("bind");
        return 1;
    }
    printf("server start!\n");

    // 循环处理收到的请求、返回响应
    while (1) {
        // 读取请求
        char buf[1024] = {0};
        struct sockaddr_in client_addr;  // 客户端 ip 和 port
        socklen_t len = sizeof(client_addr);

        ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&client_addr, &len);
        if (n < 0) {
            perror("recvfrom");
            continue;  // 当一次读取失败,服务器不退出
        }
        buf[n] = '\0';

        // 处理请求
        // 因为是回显服务器,原封不动返回给客户端

        // 返回响应
        n = sendto(fd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, len);
        if (n < 0) {
            perror("sendto");
            continue;
        }

        printf("[%s:%d] buf: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);
    }

    close(fd);
    return 0;
}

客户端实现

  1. 创建 socket;
  2. 配置要连接服务器的 ip 和 port;
  3. 循环处理:获取输入、发送给服务器、获取响应结果。
gcc echo_client.c -o echo_client
// echo_client.c
// UDP 回显客户端

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

int main() {
    // 创建 socket
    int fd = socket(AF_INET6, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    // 配置要连接服务器的 ip 和 port
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(8080);

    while (1) {
        // 获取输入
        printf("input> ");
        char input[1024] = {0};
        scanf("%s", &input);
        
        // 将输入发送给服务器
        sendto(fd, input, sizeof(input), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
        printf("req: %s\n", input);
        
        // 接收响应
        char resp[1024] = {0};
        recvfrom(fd, resp, sizeof(resp), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
        printf("resp: %s\n", resp);
    }

    close(fd);

    return 0;
}

实现说明

1、socket()

socket 函数用来创建一个建立网络通信的端点。

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数:
domain,在上面的代码中使用 AF_NET,即 IPv4,当然还有其他的选项:

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        Appletalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)

type,套接字具有指定的类型,该类型指定通信语义。即代表协议,上述代码中使用 SOCK_DGRAM 代表 UDP。

SOCK_STREAM 	TCP协议使用这个
SOCK_DGRAM 		UDP协议

protocol,用来指定 socket 所使用的传输协议编号,通常为0。

返回值:
失败返回 -1,成功返回对应的文件描述符。

2、bind()
当使用 socket() 来创建一个 socket 时候,它存在于名称空间(地址族)中,但没有分配给它地址。
bind 用来给 socket 绑定 ip 和 端口。

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

返回值:
成功返回 0,失败返回 -1。
参数:
sockfd,创建的 socket 文件描述符。
addr,这个结构体存储协议族、ip、port等信息。
addr_len,addr 的大小。

struct sockaddr {
	sa_family_t sa_family;
	char        sa_data[14];
}

socket API 可以都用 struct sockaddr * 类型表示, 在使用的时候需要强制转化成 sockaddr_in;
这样的好处是程序的通用性,可以接收IPv4、IPv6, 以及UNIX Domain Socket 各种类型的sockaddr结构体指针做为参数;
在这里插入图片描述

3、sendto()

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

发送 buf 中的数据 len 个给 dest_addr。

4、recvfrom()

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

接收到一个数据,src_addr 是输出型参数,代表谁发给我的。

封装 UDP socket

可以看到,上面服务端、客户端实现时候,有一些同样的操作要做,那为了后面使用方便,封装一个 UDP 的 socket 类来实现这些共同的操作。

来分析一下需求:
客户端程序需要:

  • 创建 socket
  • 关闭 socket
  • 接收数据
  • 发送数据

服务端程序需要:

  • 创建 socket
  • 关闭 socket
  • 接收数据
  • 发送数据
  • 绑定端口号

实现

udp_socket.hpp

#ifndef __UDP_SOCKET__
#define __UDP_SOCKET__

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

#include <string>

class UdpSocket {
public:
	UdpSocket() : m_fd(-1) {}
    ~UdpSocket() {
        Close();
    }

    // 创建一个 UDP socket
    // 成功返回 true,失败返回 false
	bool Create() {
        m_fd = socket(AF_INET, SOCK_DGRAM, 0);
        return m_fd == -1 ? false : true;
    }
    // 关闭 socket
    // 成功返回 true,失败返回 false
    bool Close() {
        return close(m_fd) == -1 ? false : true;
    }
    // 绑定 ip 和 port
    bool Bind(const std::string& ip, const std::uint16_t& port) {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        return bind(m_fd, (sockaddr*)&addr, sizeof(addr)) == -1 ? false : true;
    }
    // 接收数据
    bool RecvFrom(std::string* msg, std::string* ip=nullptr, std::uint16_t* port=nullptr) {
        char buf[1024] = { 0 };
        sockaddr_in peer;
        socklen_t peer_len = sizeof(peer);
        ssize_t n = recvfrom(m_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&peer, &peer_len);
        if (n < 0) {
            return false;
        }
        *msg = buf;
        if (ip != nullptr) {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != nullptr) {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    // 发送数据
    bool SendTo(const std::string& msg, const std::string& ip, const std::uint16_t& port) {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        ssize_t n = sendto(m_fd, msg.c_str(), msg.size(), 0, (sockaddr*)&addr, sizeof(addr));
        return n == -1 ? false : true;
    }

private:
	int m_fd;
};

#endif // __UDP_SOCKET__

测试

// 回显客户端
#include <iostream>

#include "udp_socket.hpp"

int main() {
    UdpSocket socket;
    bool ret = socket.Create();
    if (!ret) {
        perror("Socket::Create()");
        return -1;
    }
    
    while (true) {
        std::string input;
        std::cout << "input> ";
        std::cin >> input;

        ret = socket.SendTo(input, "127.0.0.1", 8080);
        if (!ret) {
            perror("Socket::SendTo()");
            continue;
        }
        
        std::string resp;
        ret = socket.RecvFrom(&resp);
        if (!ret) {
            perror("Socket::RecvFrom()");
            continue;
        }
    }

    socket.Close();
    return 0;
}

测试结果:OK
在这里插入图片描述


到此还没有结束。为了更方便的使用,我们可以再封装一下,实现一个通用的 UDP 服务器和 UDP 客户端。
首先来想一下一个 Server 都需要做哪些事情:

  1. 启动服务器(绑定端口和 ip)。
  2. 循环:读取请求、处理请求、返回响应。

这个处理请求其实是和具体业务相关的,比如回显服务器和聊天服务器,所以把这个抽出来,让用户传自己的函数进来处理。

再来想一下一个 Client 都需要做哪些事情?

  1. 发送数据、接收数据。

那就来实现吧。

通用 UDP 服务器和客户端

UDP Server

#include <cassert>
#include <functional>
#include "udp_socket.hpp"

// 处理函数
// 无返回值
// req:请求
// resp:输出参数,计算的响应结果
typedef std::function<void (const std::string& req, std::string* resp)> Handler;

// UDP 通用服务器
class UdpServer {
public:
    UdpServer() {
        // 创建一个 socket
        bool ok = m_sock.Create();
        if (!ok) {
            perror("UdpServer::UdpServer()");
        }
    }
    ~UdpServer() {
        m_sock.Close();
    }

    bool Start(const std::string& ip, const std::uint16_t& port, Handler handler) {
        bool ret = m_sock.Bind(ip, port);
        if (ret == false) {
            return false;
        }
        while (true) {
            // 接收请求
            std::string req;
            std::string peer_ip;
            std::uint16_t peer_port;
            m_sock.RecvFrom(&req, &peer_ip, &peer_port);

            // 调用处理请求函数
            std::string resp;
            handler(req, &resp);

            // 返回响应
            m_sock.SendTo(resp, peer_ip, peer_port);
        }
    }

private:
    UdpSocket m_sock;
};

UDP Client

#include "udp_socket.hpp"

class UdpClient {
public:
    UdpClient(const std::string& ip, const std::uint16_t& port) : m_sv_ip(ip), m_sv_port(port) {
        if (!m_sock.Create()) {
            perror("UdpClient::UdpClent()");
        }
    }
    ~UdpClient() { 
        if (!m_sock.Close()) {
            // 关闭失败,打印错误信息
            perror("UdpClient::~UdpClient()");
        }
    }

    bool GetResp(std::string* resp) {
        return m_sock.RecvFrom(resp);
    }
    bool SetReq(const std::string& req) {
        return m_sock.SendTo(req, m_sv_ip, m_sv_port);
    }

private:
    UdpSocket m_sock;
    std::string m_sv_ip; // 服务端ip
    std::uint16_t m_sv_port;  // 服务器port
};

测试

英译汉服务器

// 英文 -> 中文 服务器

#include <map>
#include "udp_server.hpp"

int main() {
    std::map<std::string, std::string> m;
    m["hello"] = "你好";
    m["world"] = "世界";

    UdpServer server;
    server.Start("0.0.0.0", 9090, [&m](const std::string& req, std::string* resp){
        if (m.find(req) != m.end()) {
            *resp = m[req];
        } else {
            *resp = "未找到";
        }
    });

    return 0;
}

英译汉客户端

// 英 -> 汉客户端

#include <iostream>
#include "udp_client.hpp"

int main() {
    UdpClient cli("127.0.0.1", 9090);
    while (true) {
        std::cout << "zh> ";
        std::string zh;
        std::cin >> zh;
        cli.SetReq(zh);
        std::string en;
        cli.GetResp(&en);
        std::cout << "ch: " << en << std::endl;
    }
    return 0;
}
g++ dict_server.cc -o dict_server -std=c++11
g++ dict_client.cc -o dict_client -std=c++11

在这里插入图片描述

封装后使用 UDP 服务器、客户端就方便多了!


EOF

发布了98 篇原创文章 · 获赞 91 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/Hanoi_ahoj/article/details/105358383
今日推荐