Udp网络编程

目录

一、预备知识

1.端口        

2.TCP协议和UDP协议

3.socket编程接口

①socket 常见API

②sockaddr结构

二、网络编程

1.UDP网络程序

1.1服务器

①打印

②socket​编辑

③bind

④recvfrom ​编辑

1.2客户端

①sendto

 1.3提升通信的花样性

①将字符串返还

②注册

③消息路由


一、预备知识

1.端口        

         上篇我们讲到,就是数据与数据的交互,那数据之间的交互又是用户与用户之间的交互的体现,但本质上还是用户通过进程来进行与对面用户的进程通信

        但是每个主机上的进程成千上百哪能对应准确的进程来通信?

        需要两个数据就可以准确定位。

        IP:标识主机的唯一性;

        端口号(port):标识主机内的进程的唯一性;

        IP + port = socket ->全网唯一一个进程。

        一个进程可以和多个端口号绑定,但一个端口号只能和一个进程绑定。

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

2.TCP协议和UDP协议

        这两个都是传输层协议。

        TCP协议:                                                  UDP协议

        ①有链接                                                     ①无连接

        ②可靠传输                                                 ②不可靠传输

        ③面向字节流                                             ③面向数据报

3.socket编程接口

①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);

②sockaddr结构

        我们看到这些接口中都有struct sockaddr*类型的参数,这个类型到底是什么?

        网络通信本质是不同主机进程之间的通信,那使用网络通信的接口让同一主机的进程之间通信也是可以的。

        那怎么实现呢?

        靠类型的强转实现。

        特定函数在使用结构之前,会先提取判断前16位数字,如果地址类型是AF_INET,就将struct sockaddr强转为struct sockadd_in类型,如果地址类型是AF_UNIX就强转为struct sockadd_un类型。

二、网络编程

1.UDP网络程序

        当然你想配合整体代码看的话,请点击此处的gitee浏览。

1.1服务器

①打印

        为了方便打印,我们先写一个打印函数。

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

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

void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    char logInfor[1024];
    char *name = getenv("USER");
    va_list ap;
    va_start(ap, format);

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

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

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

②socket

        目的是创建一个套接字。

domain:本地通信还是网络通信 

一般有这几个选项:

 type:套接字类型决定了通信的时候的报文类型,一般有流式、用户数据报类型。

一般有这几个选项:

protocol:协议类型。网络通信中设置为0。

返回值:成功一个文件描述符被返回,失败返回-1,errno被设置。

        首先来使用一下socket这个函数。

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

    return 0;
}

结果:

        还要建一个类,作为初始的框架

class UdpServer
{
public:
    UdpServer()
    {
    }
    ~UdpServer()
    {
    }

public:
    void init()
    {
    }
    void start()
    {
    }

private:
    int sockfd;
};

        这里的创建套接字要放到init初始化中。

​​​​​​        // 1.create 套接字
        _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);

③bind

        给一个套接字绑定上iP地址与端口号。

        // 2 bind
        // 2.1 填入基本信息到struct sockaddr_in 中
        struct sockaddr_in local;
        // 初始化
        bzero(&local, sizeof(local));
        // 填充域 AF_INET 网络通信 AF_UNIX 本地通信
        local.sin_family = AF_INET;
        // 填充对应的端口号 htons的作用是将本地序列转换为网络序列这样才能发送给对方
        local.sin_port = htons(_port);
        // 服务器的IP地址 xx.yy.aa.ccc 每个都是0-255的数字,有四个8比特位 正好放在uint36_t中
        // sin_addr也是一个结构体其中的元素是s._addr是被typedef过的uint32_t
        // INADDR_ANY就是0,一般不关注服务器绑定哪一个IP地址,服务器会自动bind,一般所有服务器都是这样做的
        // inet_addr将char *转换为s_addr,还会将主机序列转换为网络序列
        local.sin_addr.s_addr = _ip.empty() ? 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 success: %d", _sockfd);

       其中sockaddr_in的成员为:      

        其中包含端口号和IP地址。

        这时大致udpserver已经成型,我们来测试下。

static void Usage(const string proc)
{
    cout << "Usage:\n\t" << proc << " port [ip]" << endl;
}
    void start()
    {
        while (1)
        {
            logMessage(NOTICE, "udpserver runing");
            sleep(1);
        }
    }
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }

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

    return 0;
}

         这里的端口号最好不要绑定0-1023的端口号,这些端口号是服务器自己使用的对应特定服务的。         这时我们可以通过netstat -lnup来查看当前的网络服务。

④recvfrom 

        从特定socke中读取到buf里,长度为len,默认设置flags为0,阻塞式读取,src_addr用来接收发送方的参数,addrlen为src_addr的大小。返回值为读到的字节大小。

    void start()
    {
        char inbuffer[1024];  // 输入进来的数据放到inbuffer中
        char outbuffer[1024]; // 输出的数据放outbuffer中
        while (1)
        {
            struct sockaddr_in peer;      // 输出形参数
            socklen_t len = sizeof(peer); // 输入输出型参数
            ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (size > 0)
            {
                // 这里将读的数据看为字符串
                inbuffer[size] = 0;
            }
            else if (size == -1)
            {
                logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
            }
            // 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
            // peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
            string peerip = inet_ntoa(peer.sin_addr);
            uint16_t peerport = ntohs(peer.sin_port);
            // 打印客户端IP与port 和信息
            logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
        }
    }

1.2客户端

        以上写的都是服务端,现在完善一下客户端。

        上面提到过作为服务器不用bind特定的IP地址和port端口号,但是作为客户端必须知道服务器的IP地址和port端口号。

static void Usage(const string proc)
{
    cout << "Usage:\n\t"
         << "server IP ,server port" << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1. 获取服务端
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

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

    // 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
    // 2.3 填写对应服务器信息
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    // 都需要转换为网络序列
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);
    ...

①sendto

        既然到了客户端,服务端来接收,客户端就要发送。

        通过指定的套接字,从缓冲区buf中读取len的长度的内容,默认flags为0阻塞式,dest_addr为目的地。

    // 3. 发送消息
    string output;
    while (1)
    {

        cout << "Please entry | ";
        getline(cin, output);

        // 发送
        sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
    }

        写了这么多,我们来测试下。

        这里的IP地址127.0.0.1,是本地环回,就指的是本主机。

        当然这是本地之间的测试,你如果想远程测试可以,将udpclient发给他,然后将自己的IP地址告诉对方,自己打开udpserver并确定端口号,对方使用udpclient通过IP地址和端口号就可以远程通信了。当然记得使用g++进行编译时要加-static,库变成静态连接,这样对面没有对应的库也没有关系。具体的可以看我这篇博客

        那要是没有两台linux呢?我们可以用Visual Studio做出windows版本的客户端。

代码:

#pragma warning(disable:4996)// 使warning去掉
#pragma comment(lib,"ws2_32.lib")// 所需要包含的连接库

#include <iostream>
#include <cstdio>
#include <cassert>
#include <string>
#include <WinSock2.h>

using namespace std;

int serverport = 8888;
string serverip ="121.4.139.131"; // 此处要填自己主机的IP地址

int main()
{
	// 用作初始化套接字
	WSADATA data;
	// 初始套接字
	WSAStartup(MAKEWORD(2, 2), &data);
	(void)data;

     SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
     assert(sockfd > 0);

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

    string output;
    while (1)
    {
        cout << "Please entry | ";
        getline(cin, output);

        sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr*)&server, sizeof(server));
    }
    closesocket(sockfd);
    WSACleanup();
    return 0;
}

结果:


         注意:有些云服务器没有开放对应的端口,是不能使用这种Udp方法通信的。我们要在自己云服务器的官网找到你的云服务器,在你的实例中点击防火墙,然后点击添加规则,我们这次测试为Udp所以点击Udp,再将你自定义的端口输入,点击确定,这样你就可以和远处的人通信了。虽然这次实验只是单方面的通信。


 1.3提升通信的花样性

①将字符串返还

        我们服务端只接受信息太单一了,我们将发送过来的字符串,转换成大写转换回去。这些代码分别写在上一步server打印信息和client发送信息之后。

代码:

UdpServer.cc:start():            
            // 转换字符串小写-大写
            for (int i = 0; i < strlen(inbuffer); i++)
            {
                if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                {
                    outbuffer[i] = toupper(inbuffer[i]);
                }
                else
                {
                    outbuffer[i] = inbuffer[i];
                }
            }
            sendto(_sockfd, outbuffer, strlen(outbuffer), 0, (struct sockaddr *)&peer, len);
            memset(outbuffer,0,sizeof(outbuffer));
UdpClient.cc:main():
        // 接收
        char buffer[1024];
        struct sockaddr temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "Server output| " << buffer << endl;
        }

结果:               

②注册

        使用unordered_map来简单存储用户信息,来区分新老用户。

        客户端没有任何变化,服务端新增一个成员变量,将转换字符注释掉,向客户端发送字符也注释掉。注意服务端的代码都是写在类中的。

代码:

 void start()
    {
        char inbuffer[1024];  // 输入进来的数据放到inbuffer中
        char outbuffer[1024]; // 输出的数据放outbuffer中
        int i = 1;
        while (1)
        {
            struct sockaddr_in peer;      // 输出形参数
            socklen_t len = sizeof(peer); // 输入输出型参数
            ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (size > 0)
            {
                // 这里将读的数据看为字符串
                inbuffer[size] = 0;
            }
            else if (size == -1)
            {
                logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
            }
            // 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
            // peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
            string peerip = inet_ntoa(peer.sin_addr);
            uint16_t peerport = ntohs(peer.sin_port);
            // 打印客户端IP与port 和信息
            logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);

            checkOnlineUser(i,peerip, peerport, peer);
        }
    }  
    bool checkOnlineUser(int &i,string &ip, uint16_t port, struct sockaddr_in &peer)
    {
        string userInfor = ip;
        userInfor += " ";
        userInfor += to_string(port);

        auto iter = user.find(userInfor);
        if (iter == user.end())
        {
            // 没找到
            user.insert({userInfor, peer});
            i = 1;
            if (i == 1)
            {
                cout << "新用户登录" << endl;
            }
        }
        else
        {
            // 找到了
            if (i == 1)
            {
                i = 0;
                cout << "老用户登录" << endl;
            }
        }
    }
private:
    unordered_map<string, struct sockaddr_in> user;

③消息路由

        将输出与命名管道结合起来。

        先在当前路径建立命名管道,修改代码,服务端中将客户端发回的消息使用管道保存起来。代码:

    void start()    
    {
        ......
        logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
        checkOnlineUser(i, peerip, peerport, peer);
        messageRoute(inbuffer);
    }
    void messageRoute(string message)
    {
        for (auto &ch : user)
        {
            sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(ch.second),sizeof(ch.second));
        }
    }

结果:

         我们呢,还可以使用线程来改造下。让主线程不断在发消息,另一个线程去接收发回来的消息。 

代码:

struct sockaddr_in server;

static void Usage(const string proc)
{
    cout << "Usage:\n\t"
         << "server IP ,server port" << endl;
}

void *fuc(void *argc)
{
    while (1)
    {
        int sockfd = *(int *)argc;
        // 接收
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "Server output| " << buffer << endl;
        }
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1. 获取服务端
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

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

    // 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
    // 2.3 填写对应信息

    bzero(&server, sizeof(server));
    // 都需要转换为网络序列
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    pthread_t t;
    pthread_create(&t, nullptr, fuc, (void *)&sockfd);

    // 3. 发送消息
    string output;
    while (1)
    {

        cerr << "Please entry | ";
        getline(cin, output);

        // 发送
        sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
    }
    close(sockfd);
    return 0;
}

         运行过程与上文的结果一样,这里就不演示了。

        这里的fifo好像没什么用啊?为什么要管道呢?

        我们启动服务器,在客户端输入消息,服务器再将消息返回,使用fifo来展示返回的消息。一个人这样使用其实没有fifo用处,但如果是多个人使用,无论谁向客户端输入消息,服务器都会返还所有它接受的消息到fifo中,意味着我们可以通过fifo来查看别人发的消息,这样不就可以双端聊天了嘛。至于fifo中不显示谁发送的消息,我们将server中的sendto所使用的buffer中填写从recevfrom中获取到的IP地址与端口,就可以辨识是谁发送的消息。

        到这里我们Udp网络程序的编写暂时告一段落基本的要求我们都已经实现,Udp的全部代码我已上传gitee,有兴趣的可以看一下。  

        限于篇幅,Tcp网络编程就移至下一节去讲,感谢观看,如有错误请指出,我们下次再见。      

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值