<UDP网络编程>——《计算机网络》

目录

1. 网络基础知识

1.1 理解源IP地址和目的IP地址

1.2 认识端口号

1.3 理解 "端口号" 和 "进程ID"

1.3.1 理解源端口号和目的端口号

1.4 认识TCP协议

1.5 认识UDP协议

1.6 网络字节序

2. socket编程接口

2.1 socket 常见API

2.2 sockaddr结构

2.3 sockaddr 结构

2.4 sockaddr_in 结构

2.5 in_addr结构

3. 简单的UDP网络程序

3.1 封装 UdpSocket

3.2 UDP通用服务器

3.3 实现英译汉服务器

3.4 UDP通用客户端

3.5 实现英译汉客户端  

3.6 地址转换函数

3.7 关于inet_ntoa

4. udp网络编程

4.1 网络接口函数及测试

4.2 UDP程序完整代码

后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


本节重点

  • 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
  • 学习socket api的基本用法;
  • 能够实现一个简单的udp客户端/服务器;

1. 网络基础知识

1.1 理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析。

在进行网络通信的时候,不仅需要考虑两台主机间互相交互数据,本质上,进行数据交互是用户与用户在进行交互!而用户的身份通常书程序体现的,程序在运行中就是在进程中,

主机间通信的本质是:在各自的主机上的进程在交互数据!

IP地址可以完成主机与主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方!

1.2 认识端口号

端口号(port)是传输层协议的内容.
  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.

IP:确保主机的唯一性
port:确保该主机上的进程的唯一性

IP-PORT: 标识互联网中唯一的一个进程       ——> socket  ——>网络通信的本质也是进程间通信!

1.3 理解 "端口号" 和 "进程ID"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

1.3.1 理解源端口号和目的端口号

送快递例子
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";

1.4 认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

1.5 认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.6 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2. socket编程接口

2.1 socket 常见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);

2.2 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

2.3 sockaddr 结构

2.4 sockaddr_in 结构

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

2.5 in_addr结构

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数

3. 简单的UDP网络程序

实现一个简单的英译汉的功能
备注: 代码中会用到 地址转换函数 . 参考接下来的章节

3.1 封装 UdpSocket

udp_socket.hpp:

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket
{
public:
    UdpSocket() : fd_(-1)
    {
    }
    bool Socket()
    {
        fd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd_ < 0)
        {
            perror("socket");
            return false;
        }
        return true;
    }
    bool Close()
    {
        close(fd_);
        return true;
    }
    bool Bind(const std::string &ip, 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);
        int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));
        if (ret < 0)
        {
            perror("bind");
            return false;
        }
        return true;
    }
    bool RecvFrom(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL)
    {
        char tmp[1024 * 10] = {0};
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0, (sockaddr *)&peer, &len);
        if (read_size < 0)
        {
            perror("recvfrom");
            return false;
        }
        // 将读到的缓冲区内容放到输出参数中
        buf->assign(tmp, read_size);
        if (ip != NULL)
        {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != NULL)
        {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    bool SendTo(const std::string &buf, const std::string &ip, 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 write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr *)&addr, sizeof(addr));
        if (write_size < 0)
        {
            perror("sendto");
            return false;
        }
        return true;
    }

private:
    int fd_;
};

3.2 UDP通用服务器

udp_server.hpp:

 

#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lambda
#include <functional>
typedef std::function<void(const std::string &, std::string *resp)> Handler;
class UdpServer
{
public:
    UdpServer()
    {
        assert(sock_.Socket());
    }
    ~UdpServer()
    {
        sock_.Close();
    }
    bool Start(const std::string &ip, uint16_t port, Handler handler)
    {
        // 1. 创建 socket
        // 2. 绑定端口号
        bool ret = sock_.Bind(ip, port);
        if (!ret)
        {
            return false;
        }
        // 3. 进入事件循环
        for (;;)
        {
            // 4. 尝试读取请求
            std::string req;
            std::string remote_ip;
            uint16_t remote_port = 0;
            bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
            if (!ret)
            {
                continue;
            }
            std::string resp;
            // 5. 根据请求计算响应
            handler(req, &resp);
            // 6. 返回响应给客户端
            sock_.SendTo(resp, remote_ip, remote_port);
            printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port, req.c_str(), resp.c_str());
        }
        sock_.Close();
        return true;
    }

private:
    UdpSocket sock_;
};

3.3 实现英译汉服务器

以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了.
dict_server.cc:
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
    auto it = g_dict.find(req);
    if (it == g_dict.end())
    {
        *resp = "未查到!";
        return;
    }
    *resp = it->second;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage ./dict_server [ip] [port]\n");
        return 1;
    }
    // 1. 数据初始化
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "一种面向对象程序语言"));
    g_dict.insert(std::make_pair("Linux", "一种操作系统"));
    // 2. 启动服务器
    UdpServer server;
    server.Start(argv[1], atoi(argv[2]), Translate);
    return 0;
}

3.4 UDP通用客户端

 udp_client.hpp:

#pragma once
#include "udp_socket.hpp"
class UdpClient
{
public:
    UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
    {
        assert(sock_.Socket());
    }
    ~UdpClient()
    {
        sock_.Close();
    }
    bool RecvFrom(std::string *buf)
    {
        return sock_.RecvFrom(buf);
    }
    bool SendTo(const std::string &buf)
    {
        return sock_.SendTo(buf, ip_, port_);
    }

private:
    UdpSocket sock_;
    // 服务器端的 IP 和 端口号
    std::string ip_;
    uint16_t port_;
};

3.5 实现英译汉客户端  

dict_client.cc:

 

 

3.6 地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

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

3.7 关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
多线程调用inet_ntoa代码示例如下:

 

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;

void *Func1(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }
    return NULL;
}
void *Func2(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}
int main()
{
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    pthread_create(&tid1, NULL, Func1, &addr1);
    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

makefile: 

.PHONY:all
all:dictclient dictserver inetntoa

dictclient:dict_client.cc
	g++ -o $@ $^ -std=c++11
dictserver:dict_server.cc
	g++ -o $@ $^ -std=c++11
inetntoa:inet_ntoa.cc
	g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
	rm -f dictclient dictserver inetntoa

 

4. udp网络编程

4.1 网络接口函数及测试

测试1:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"

class UdpServer
{
public:
    UdpServer()
    {}
    ~UdpServer()
    {}
public:
    void init()
    {}
    void start()
    {}       
private:
    int sockfd_;
};
int main(int argc, char *argv[])
{
    // UdpServer svr;
    // svr.init();
    // svr.start();

    int fd=socket(AF_INET,SOCK_DGRAM,0);
    if(fd < 0)
    {
        logMessage(FATAL,"%s:%d",strerror(errno),fd);
        exit(1);
    }
    logMessage(DEBUG,"socket create success! fd: %d",fd);
    return 0;
}

 

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);

    va_end(ap); // ap = NULL


    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);

    // char *s = format;
    // while(s){
    //     case '%':
    //         if(*(s+1) == 'd')  int x = va_arg(ap, int);
    //     break;
    // }
}

测试2:

 

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Log.hpp"

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}

/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:
    UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
    {
    }
    ~UdpServer()
    {
    }

public:
    void init()
    {
        // 1. 创建socket套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        logMessage(DEBUG, "socket create success! sockfd_: %d", sockfd_);
        // 2. 绑定网络信息,指明ip+port
        // 2.1 先填充基本信息到 struct sockaddr_in
        struct sockaddr_in local;     // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
        bzero(&local, sizeof(local)); // memset
        // 填充协议家族,域
        local.sin_family = AF_INET;
        // 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
        local.sin_port = htons(port_);
        // 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip
        // INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
        // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
        local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
        // 2.2 bind 网络信息
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
        {
            logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
            exit(2);
        }
        logMessage(DEBUG, "socket bind success! sockfd_: %d", sockfd_);
        // done
    }

    void start()
    {
        // 服务器设计的时候,服务器都是死循环
        // demo1
        while (true)
        {
            logMessage(NOTICE, "server 提供 service 中....");
            sleep(1);
        }
    }

private:
    // 服务器必须得有端口号信息
    uint16_t port_;
    // 服务器必须得有ip地址
    std::string ip_;
    // 服务器的socket fd信息
    int sockfd_;
    // onlineuser
    std::unordered_map<std::string, struct sockaddr_in> users;
};

// struct client{
//     struct sockaddr_in peer;
//     uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }

// ./udpServer port [ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }

    UdpServer svr(port, ip);
    svr.init();
    svr.start();

    return 0;
}

 

 

 

 

 

4.2 UDP程序完整代码

udpServer.cc:

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Log.hpp"

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}

/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:
    UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
    {
    }
    ~UdpServer()
    {
    }

public:
    void init()
    {
        // 1. 创建socket套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        logMessage(DEBUG, "socket create success: %d", sockfd_);
        // 2. 绑定网络信息,指明ip+port
        // 2.1 先填充基本信息到 struct sockaddr_in
        struct sockaddr_in local;     // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
        bzero(&local, sizeof(local)); // memset
        // 填充协议家族,域
        local.sin_family = AF_INET;
        // 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
        local.sin_port = htons(port_);
        // 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip
        // INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
        // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
        local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
        // 2.2 bind 网络信息
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
        {
            logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
            exit(2);
        }
        logMessage(DEBUG, "socket bind success: %d", sockfd_);
        // done
    }

    void start()
    {
        // 服务器设计的时候,服务器都是死循环
        char inbuffer[1024];  //将来读取到的数据,都放在这里
        char outbuffer[1024]; //将来发送的数据,都放在这里
        while (true)
        {
            struct sockaddr_in peer;      //输出型参数
            socklen_t len = sizeof(peer); //输入输出型参数

            // demo2
            //  UDP无连接的
            //  对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                inbuffer[s] = 0; //当做字符串
            }   
            else if (s == -1)
            {
                logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);
                continue;
            }
            // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
            std::string peerIp = inet_ntoa(peer.sin_addr);       //拿到了对方的IP
            uint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的port

            checkOnlineUser(peerIp, peerPort, peer); //如果存在,什么都不做,如果不存在,就添加

            // 打印出来客户端给服务器发送过来的消息
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);

            //大小写转化测试
            // for(int i = 0; i < strlen(inbuffer); i++)
            // {
            //     if(isalpha(inbuffer[i]) && islower(inbuffer[i])) outbuffer[i] = toupper(inbuffer[i]);
            //     else outbuffer[i] = toupper(inbuffer[i]);
            // }
            messageRoute(peerIp, peerPort,inbuffer); //消息路由

            // 线程池!

            // sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);

            // demo1
            // logMessage(NOTICE, "server 提供 service 中....");
            // sleep(1);
        }
    }

    void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
    {
        std::string key = ip;
        key += ":";
        key += std::to_string(port);
        auto iter = users.find(key);
        if(iter == users.end())
        {
            users.insert({key, peer});
        }
        else
        {
            // iter->first, iter->second->
            // do nothing
        }
    }

    void messageRoute(std::string ip, uint32_t port, std::string info)
    {

        std::string message = "[";
        message += ip;
        message += ":";
        message += std::to_string(port);
        message += "]# ";
        message += info;

        for(auto &user : users)
        {
            sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
        }
    }
            
private:
    // 服务器必须得有端口号信息
    uint16_t port_;
    // 服务器必须得有ip地址
    std::string ip_;
    // 服务器的socket fd信息
    int sockfd_;
    // onlineuser
    std::unordered_map<std::string, struct sockaddr_in> users;
};

// struct client{
//     struct sockaddr_in peer;
//     uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
// }

// ./udpServer port [ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }

    UdpServer svr(port, ip);
    svr.init();
    svr.start();

    return 0;
}

// struct ip
// {
//     uint32_t part1:8;
//     uint32_t part2:8;
//     uint32_t part3:8;
//     uint32_t part4:8;
// }
// struct ip ip_;
// ip_.part1 = s.substr();

udpClient.cc:

#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>

struct sockaddr_in server;

static void Usage(std::string name)
{
    std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1. 根据命令行,设置要访问的服务器IP
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    // 2. 创建客户端
    // 2.1 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

    // 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
    // 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
    // 如果我非要自己bind呢?可以!严重不推荐!
    // 所有的客户端软件 <-> 服务器 通信的时候,必须得有 client[ip:port] <-> server[ip:port]
    // 为什么呢??client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了
    // 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!
    // 2.2 填写服务器对应的信息

    bzero(&server, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    pthread_t t;
    pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
    // 3. 通讯过程
    std::string buffer;
    while (true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);
        // 发送消息给server
        // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
        sendto(sockfd, buffer.c_str(), buffer.size(), 0, (const struct sockaddr *)&server, sizeof(server)); 
    }

    close(sockfd);
    return 0;
}

Log.hpp:

#pragma once

#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);

    va_end(ap); // ap = NULL


    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);

    // char *s = format;
    // while(s){
    //     case '%':
    //         if(*(s+1) == 'd')  int x = va_arg(ap, int);
    //     break;
    // }
}

makefile:

 

.PHONY:all
all:udpClient udpServer

udpClient:udpClient.cc
	g++ -o $@ $^ -lpthread -std=c++11
udpServer:udpServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f udpClient udpServer

 

后记:
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知

猜你喜欢

转载自blog.csdn.net/m0_57859086/article/details/128525896