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;
}

尝试运行一下:

完整代码

完整代码奉上

相关推荐
贞明电子11 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
明天也要努力学习11 小时前
HTTP 协议的基本格式和 fiddler 的用法
网络协议·http·fiddler
HideF11 小时前
Linux
linux·服务器·网络
万联WANFLOW11 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
听风说ml11 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
sbjdhjd11 小时前
网工进阶笔记:H3CSE 园区网全栈技术与协议汇总
网络·笔记·云计算·ssh·网络工程·vlan·aaa
taktok_ai13 小时前
用 cc-switch 连接 Taktok
网络
小嘉丶学长16 小时前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节
pt104317 小时前
Cisco设备网络安全系列-18:SIEM与SOAR安全解决方案
网络·安全
Tel1992530800417 小时前
温度湿度信号转WiFi/MQTT/Modbus TCP采集模块 无线传输
网络·网络协议·tcp/ip·工业自动化·工控设备