Socket编程UDP

V1版本-EchoServer

我们先实现一个从client输出到server然后server将client输出的数据又返回的非常简单的网络功能。

然后我们先封装一下我们的服务端。

nocopy

首先我们要确保实现的类不能被拷贝。那么我们可以继承一个不能被拷贝的父类nocopy:

cpp 复制代码
#pragma once
class nocopy
{
public:
    nocopy() {}
    ~nocopy() {}
    nocopy(const nocopy &) = delete;
    const nocopy &operator=(const nocopy &) = delete;
};

IniteServer

创建套接字

首先我们可以确定我们要在main函数里用什么功能:

那么我们现在就要开始实现初始化功能。

首先我们肯定要创建我们的套接字:

第一个参数是套接字的域,可以指定套接字使用的网络协议族,我们传入:

表示IPv4 协议族,即告诉OS IPv4通信。

第二个参数是套接字类型,即数据传输方式,我们传入:

代表无连接、不可靠、数据报,这不就是我们的UDP协议。

第三个参数protocol为具体协议类型,通常传入0即可,该函数会根据第一二个参数自动推导协议。如我们AF_INET+SOCK_DGRAM就是UDP协议。

此外我们还要注意他的返回值:

注意到返回的是文件描述符。他实际上和我们的管道类似,会创建一个内核资源进行管理。

其实我们也可以简单理解称将网卡这个硬件打开了,毕竟Linux一切皆文件。

总之我们的要有一个成员变量存放返回的文件描述符:

这时候我们就要创建套接字了,如果创建失败的话自然要输出错误信息。这里就能用上之前写的日志了。

这里SOCKET_ERROR就是我们自定义的错误码。

绑定

创建了套接字之后我们就要绑定对应的IP+端口了.

我们先回顾一下struct sockaddr。因为socket适用于各种协议的网络通信,但是各协议的地址格式不同。我们将所有地址都封装到struct sockaddr中。虽然地址格式不同,但他们首16个字节都是相同的类型,即地址类型:

我们统一用sockaddr接收,然后在前十六个字节中表明自己是什么地址,最后强转即可,很明显就是C语言的多态

我们IPV4通信自然要用sockaddr_in结构体,然后我们要对其初始化一些数据。先来看看这个结构体有什么成员:

第一个参数有点神秘,这是我们的宏函数的高亮,转到定义后看到:

是一个将sa_prefix拼接family的宏函数。

因此我们sockaddr_in的第一个成员就是sin_family。sin_family就是我们的协议家族,实际上就是我们要填的地址类型,传入AF_INET即可。

第二个参数就是我们的端口好了,传入一个自定义的2字节整数即可。

第三个参数就是我们的家族地址,这里自然就是IPv4地址。

可以看到我们存储ipv4地址的是一个无符号整型。

但是我们用户通常喜欢传入点分十进制,我们要将其转换,不过库函数已经帮我们做了这件事了:

综上我们的参数初始化就是:

果真如此吗?

我们可是要进行网络通信的,这些能确保都是网络字节序吗?

很显然端口号还不能确保是网络字节序,所以我们要对这个无符号16位整型用一下htons(short类型从主机字节序转网络字节序):

然后就可以绑定了:

由于我们地址类型不同,大小自然也不同,所以还是要换入addr的大小的。

然后做一下出错判断即可:

Start

我们要管理一个运行状态,bool _running。

当运行时就将其置为真。

那么接下来就要开始接受信息了:

这几个参数都太显然了,我就不赘述了。

我们现在也可以简单跑一下,看一下会不会形成对应的套接字:

然后用netstat -upa指令:

然后我们就可以将收到的消息发送回client了:

Client

接下里我们实现客户端的功能:

那么接下来我们要明确一些概念。

首先呢我们的客服端要想和服务器通信,必须要预先知道服务器的ip地址和端口号!

客户端的端口号通常不能让用户自行设置,而是要交由OS随机分配。

为什么呢?

设想一下如果客户端由用户自己分配,会不会发展出客户端端口号和服务器端口号绑定的事情,然后端口号肯定就不够用了,发生冲突的概率就大了。

此外,客户端也要绑定自己的ip地址和端口号,但不需要显式绑定。在我们第一次向服务器发送信息时,OS就会帮我们绑定。

INADDR_ANY

刚才提到了我们客户端要预先知道服务器的ip地址,但是我们服务器可能有不同的ip地址。如果服务器只绑定一个ip地址显然是不好的,因此我们可以绑定INADDR_ANY(宏定义的0),意味着绑定服务器的任意IP地址:

让我们继续回归客户端的编写。

现在我们可以通过main函数选项传入服务器的ip地址和端口号:

那么完整代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
        exit(0);
    }

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

    sockaddr_in server;
    memset(&server, 0, sizeof(int));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }

    while (1)
    {
        std::string line;
        std::cout << "Please Enter# ";
        std::getline(std::cin, line);

        int n = sendto(sockfd, line.c_str(), sizeof(line), 0, (struct sockaddr *)&server, sizeof(server));
        if (n > 0)
        {
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            char buffer[1024];
            int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&temp, &len);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else
            {
                break;
            }
        }
        else
        {
            break;
        }
    }

    ::close(sockfd);
    return 0;
}

接下来就可以测试下了:

127.0.0.1 是 IPv4 协议中定义的本地回环地址(Loopback Address),属于回环地址段 127.0.0.0/8 中的代表地址,其核心作用是让本机进程与自身通信,不经过任何物理网卡。

我们也可以用内网ip:

注意我们不能也不要绑定云服务器的公网ip。

InetAddr

上面我们获取peer的ip和接口方式过于丑陋了,简单封装一下吧:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>

class InetAddr
{
private:
    void ToHost(const struct sockaddr_in &addr)
    {
        _port = addr.sin_port;
        _ip = inet_ntoa(addr.sin_addr);
    }

public:
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        ToHost(addr);
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

这样我们就能直接通过ip和port接口获取对应ip地址和端口号了。

V2版本-DictServer

我们获取数据自然不是返回数据这么简单,我们要处理一些业务。我们这里就实现一个简单的翻译功能。

我们一般需要将服务器的IO逻辑和业务逻辑解耦。传入包装器即可。

dict.txt

要翻译自然需要一个词典,我们这里就简单构造一个词典吧:

复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

Dict

接下来我们就要封装词典类。

自然我们要建立一个中英文映射关系,我们可以用一个哈希表.

然后我们还要维护词典文件的路径:

那么初始化的时候,我们要将文件里的映射关系载入到哈希表中:

最后实现翻译功能即可:

运行

将刚实现的翻译功能传入io端,就完整实现了我们的在线翻译。

来试行一下吧:

非常完美!

V3版本-简单聊天室

我们最后实现一个群聊功能。

我希望的是客户端发信息给服务器,服务器维护一个在线用户列表。然后将客户端发来的信息和在线用户列表交给路由和转发模块,这个模块通过线程池的方式将信息转发给所有在线列表的用户。

Route

接下来我们就要实现我们的转发逻辑。

首先做一些基本处理:

事实上我们是要准备实现多线程的话,自然要保证线程安全,我们在用代码的时候可以先上锁。

然后我们实现各个部分逻辑:

CheckOnlineUser:

cpp 复制代码
void CheckOnlineUser(InetAddr &who)
    {
        LockGuard loakguard(&_mutex);
        for (auto &user : _online_user)
        {
            if (user == who)
            {
                LOG(DEBUG, "%s is exits\n", who.AddStr().c_str());
                return;
            }
        }
        LOG(DEBUG, "%s is not exits,add it\n", who.AddStr().c_str());
        _online_user.emplace_back(who);
    }

Offline:

cpp 复制代码
void Offline(InetAddr &who)
    {
        LockGuard loakguard(&_mutex);
        auto iter = _online_user.begin();
        for (; iter != _online_user.end(); iter++)
        {
            if (*iter == who)
            {
                LOG(DEBUG, "%s is offline\n", who.AddStr().c_str());
                _online_user.erase(iter);
                break;
            }
        }
    }

ForwardHelper:

cpp 复制代码
void ForwardHelper(int sockfd, const std::string message, InetAddr who)
    {
        LockGuard loakguard(&_mutex);
        std::string send_message = "[" + who.AddStr() + "]# " + message;
        for (auto &user : _online_user)
        {
            struct sockaddr_in peer = user.Addr();
            LOG(DEBUG, "Forward message to %s, message is %s\n", user.AddStr().c_str(), send_message.c_str());
            sendto(sockfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&peer, sizeof(peer));
        }
    }

Forward:

cpp 复制代码
void Forward(int sockfd, const std::string &message, InetAddr &who)
    {
        // 1.判断该用户是否在用户列表中?如果在,不做处理,如果不在,加入在线列表
        CheckOnlineUser(who);

        // 如果发送QUIT或Q则退出在线列表
        if (message == "QUIT" || message == "Q")
        {
            Offline(who);
        }

        // 2.转发信息
        // ForwardHelper(sockfd,message);
        task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);
        ThreadPool<task_t>::GetInstance()->Equeue(t);
    }

Server

现在就可以调整服务器的Start逻辑了:

Client

我们要实现客户端首先要想到一件事。

我们发送消息的时候线程是在阻塞等待的,这意味着我们不发送消息就无法接受消息。因此我们要开两个线程,一个线程发送消息,一个线程接受消息。

我们还可以将收到的消息重定向到另一个终端,这样发送消息的终端就不会被接受的消息污染。

完整逻辑

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"

using namespace ThreadMoudle;

int InitClient()
{
    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(1);
    }
    return sockfd;
}

void RecvMessage(int sockfd, const std::string &name)
{
    while (true)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buffer[1024];
        int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cerr << buffer << std::endl;
        }
        else
        {
            std::cerr << "recvfrom error" << std::endl;
            break;
        }
    }
}

void SendMessage(int sockfd, std::string serverip, uint16_t serverport, const std::string &name)
{
    struct sockaddr_in server;
    memset(&server, sizeof(server), 0);
    server.sin_family = AF_INET;
    server.sin_port = serverport;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    std::string cli_profix = name + "# ";
    while (true)
    {
        std::string line;
        std::cout << cli_profix;
        std::getline(std::cin, line);
        int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (n < 0)
            break;
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
        exit(0);
    }

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

    Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));
    Thread send("send-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));

    recver.Start();
    send.Start();
    
    recver.Join();
    send.Join();

    ::close(sockfd);

    return 0;
}

尝试运行一下:

完整代码

完整代码奉上

相关推荐
roman_日积跬步-终至千里2 小时前
【源码分析】StarRocks EditLog 写入与 Replay 完整流程分析
java·网络·python
车载测试工程师2 小时前
CAPL学习-AVB交互层-媒体函数1-回调&基本函数
网络·学习·tcp/ip·媒体·capl·canoe
努力进修2 小时前
【JavaEE初阶】UDP协议和TCP协议
tcp/ip·udp·java-ee
爱尔兰极光2 小时前
计算机网络--TCP传输
网络·tcp/ip·计算机网络
醉舞经阁半卷书12 小时前
zookeeper服务端配置
网络·分布式·zookeeper
晚风(●•σ )3 小时前
【华为 ICT & HCIA & eNSP 习题汇总】——题目集27
网络·计算机网络·网络安全·华为
寰天柚子10 小时前
裸金属服务器深度解析:适用场景、选型指南与运维实践
服务器·网络·github
GTgiantech10 小时前
精准成本控制与单向通信优化:1X9、SFP单收/单发光模块专业解析
运维·网络
Suchadar10 小时前
ACL访问控制列表协议
网络·智能路由器