网络实践——Socket编程UDP

文章目录

Socket编程UDP

在了解了相关的网络基础知识后,我们不会像学系统知识一样,先学原理,再讲应用。

我们先先不选择学习网络原理。我们先来试着学习一下使用接口来完成一些简单地实践。

在这个部分中,我们不只使用网络相关知识,而是结合前面系统部分学习时,完成的一些组件代码来使用!这些我们后面会见到的!

UDP接口的使用铺垫

先说一下,有了网络基础知识 + 系统部分的知识。其实我们只需要了解一下UDP相关接口的使用,其实就能较为熟练地学习如何使用。下面,我们将把网络中将用的接口进行简单讲解。

socket

c 复制代码
NAME
       socket - create an endpoint for communication //打开通信的一端

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

这个文件,是用来创建套接字的!我们在网络基础部分讲过,两个进程的通信是基于套接字(端口号 + ip)的!具体的创建,就是用这个接口。

第一个参数domain是选择使用什么域来进行通信:

这里我们记住是选择AF_INET进行网络通信即可!

第二个参数type是选择使用什么方式传输,比如TCP/UDP:

字符流 -> SOCK_STREAM -> TCP

数据包 -> SOCK_DGRAM -> UDP

第三个参数protocol是要我们选择协议,默认给0就是TCP/IP了,记住即可!

返回值:

成功,返回一个file descriptor,即文件描述符!即使我们不知道这个网络通信的原理,但是我们至少可以直到,当前进程的文件描述符表定会有一个位置指向socket文件!

所以,我们就把它当成特殊的网络文件来使用!这符合 Linux下一切皆文件!

recvform && sendto

我们先来看着两个函数的相关信息,它们具有较强相似性:

recvform

c 复制代码
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

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

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

RETURN VALUE
  		These calls return the number of bytes received, or -1 if an error occurred.  
  		In the event of an error, errno is set to indicate the error.

sendto

c 复制代码
NAME
       send, sendto, sendmsg - send a message on a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

RETURN VALUE
	On success, these calls return the number of bytes sent.  
	On error, -1 is returned, and errno is set appropriately.

见名思意:
recvform是从套接字文件中获取内容;sendto是向套接字文件中发送内容。


参数解释:

  1. int sockfd,就是对应的套接字文件!这个不需要解释。

  2. const void *buf,这个就是一个缓冲区,recvfrom把从套接字文件中读到的内容读取到该缓冲区。sendto将该缓冲区内的内容发送到套接字文件。

  3. size_t len,即缓冲区长度。

  4. int flags,这个是选择是否阻塞读取/发送的。默认给0就是阻塞。也就是说,recvfrom读不到内容就会阻塞,sendto没有内容发送也会阻塞。这个我们在系统那里也见过。

  5. struct sockaddr,这个其实我们早在网络基础部分的时候,就已经讲到过这个。我们说了,设计socket网络通信的时候,为了能够让网络通信和本地通信 公用一套接口,所以设计了这么个c语言版的基类

    所以,如果需要收发消息,其实对方进程的相关信息:ip、端口号、通信协议,都会在对应的结构体上体现出来。所以,未来在使用socket通信的时候,如何知道或者发送自己的相关信息,就是靠着这个结构体sockaddr_in或者sockaddr_un,强转类型为sockaddr对应的地址变量后,然后进行相关操作!

6.socklen_t *addrlensocklen_t addrlen

6.1. 对于recvfrom收消息来说,参数是socklen_t *addrlen,这很明显是一个指针地址变量!所以,是需要我们把sockaddr_in的地址传进去给第五个参数,然后需要定义一个变量指明该结构体大小,传入地址给第六个参数。

Tips:因为该变量是输出型参数,实际上它会返回真正读到的字节数(因为网络传输可能丢包)

6.2. 对于sendto发消息来说,这个就没什么好说的了,本身数据就在,直接传对应的大小即可

bind

c 复制代码
NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

RETURN VALUE
       On success, zero is returned.  
       On error, -1 is returned, and errno is set appropriately.

没看错,这里还有一个接口叫做bind,但是,这个不是c++11后提出的std::bind

这个接口最重要的就是,将套接字文件和对应的进程的IP地址和端口号(在结构体sockaddr_in内存储了),绑定到对应的套接字文件中!

​​bind的作用 ​​:

显式绑定:bind()将套接字固定到特定IP+端口,成为该地址的唯一接收者。

未绑定时:系统在首次发送数据时自动分配随机端口(IP默认为所有本地接口)

这里理解一下就好,实在看不懂的话等后续讲解的时候再说一遍如何使用。现在只需要学会使用即可,原理等后期再来讲解!

字节序转化使用(Tips)

这个在网络基础中也是提到了。这里再多提醒一下,因为后续的实践中,必然是有从网络序列转主机序列的内容,也有主机序列转网络序列的!所以,这些是必须使用的!

实践部分

version_1@echo_server

源码:version_1@echo_server

第一个版本,我们希望实现一种功能:

即有一个服务器,然后其余所有的进程都可以给其发消息,然后服务器处理后再转发给对应的进程,这样子就起到了一个回显服务器的效果!

所有的过程都已经在代码的注释中展现出来了。

version_2@dict_server

源码:version_2@dict_server

第二个版本,其实就是进行一次解耦合!让第一个版本中对于信息回显的处理模块,转化为服务器调用词典翻译模块!其实主要的逻辑和第一个版本差别不大。

只不过是引入多了一个模块,让词典从对应的配置文件中读取对应信息,然后服务器进行调用后再进行转发结果给请求该服务的进程!

version_3@chat_server

源码:version_3@chat_server

第三个版本我们来详细说明一下:

第三个版本我们希望做到一个群聊转发功能,即服务器在接收到某个客户端发送来的消息后,要转发给所有处于在线的用户。

1.我们这里规定:

默认第一次发送消息的就是要加入群聊的。服务器接收到消息之后,要怎么样才能转发给所有的在线用户呢?-> 添加一层路由层,用于管理在线用户(组织描述)和消息转发!

2.但是,我们觉得效率太低了,所以希望的是,服务器将收到的信息推给后端线程池,让线程池自行调用路由转发功能!所以引入了线程池。

3.此前版本1、2写的客户端使用代码是有问题的!因为强行规定了先发消息才能收消息。所以,在实现群聊过程中,发现客户端只有发了消息,才能接收到其他客户端被转发的消息。所以,为此我们进行了处理,就是让客户端多线程处理!即创建两个线程,同时进行收发!

4.线程池访问路由表的时候(就是底层的一个哈希),也是会涉及到线程安全的。但是,因为今天的实现并没有规定消息的协议(是否退出、私法、群发、还是请求服务器处理...)。我们仅仅只是把收到的内容当字符串处理!

但是不管怎么说,线程池内每个线程访问的时候,是会出现数据不一致的问题的!因为STL不是线程安全的,所以我们这里就粗暴一点,直接加锁!

5.实践的时候,因为我没有多台Linux机器,所以,使用了Windows进行辅助测试,测试是否能够跨网通信!代码如下:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>


#pragma warning(disable : 4996)

#pragma comment(lib, "ws2_32.lib")

std::string serverip = "";  // 填写你的云服务器ip
uint16_t serverport = ; // 填写你的云服务开放的端口号


SOCKET sockfd; 
struct sockaddr_in server;

void recv_msg() {
    while (1) {
        char buffer[1024];
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if (s > 0){
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }
}


void send_msg() {
       
    memset(&server, 0, sizeof(server)); 
    server.sin_family = AF_INET; 
    server.sin_port = htons(serverport); //? 
    server.sin_addr.s_addr = inet_addr(serverip.c_str()); 

    while (1) {
        std::string message; 
        std::getline(std::cin, message);
        if (message.empty()) continue; 
        sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    }
}









int main() {
    WSADATA wsd; 
    WSAStartup(MAKEWORD(2, 2), &wsd); 



    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == SOCKET_ERROR){
        std::cout << "socker error" << std::endl;
        return 1;
    }




    std::thread Recv(recv_msg);
    std::thread Send(send_msg);

    
    Recv.join();
    Send.join();

    closesocket(sockfd);
    WSACleanup();

    return 0;
}

//int main()
//{
//    WSADATA wsd;
//    WSAStartup(MAKEWORD(2, 2), &wsd);
//
//
//    memset(&server, 0, sizeof(server));
//    server.sin_family = AF_INET;
//    server.sin_port = htons(serverport); //?
//    server.sin_addr.s_addr = inet_addr(serverip.c_str());
//
//    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//    if (sockfd == SOCKET_ERROR)
//    {
//        std::cout << "socker error" << std::endl;
//        return 1;
//    }
//
//
//
//    std::string message;
//    char buffer[1024];
//    while (true)
//    {
//        std::cout << "please input: ";
//        std::getline(std::cin, message);
//        if (message.empty()) continue;
//        sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//        struct sockaddr_in temp;
//        int len = sizeof(temp);
//        int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
//        if (s > 0)
//        {
//            buffer[s] = 0;
//            std::cout << buffer << std::endl;
//        }
//    }
//
//    closesocket(sockfd);
//    WSACleanup();
//    return 0;
//}

前面一份是用于版本3的测试,后面是用于版本1、2的测试!这里可以搜一下相关大模型了解一下用法,其实用法和Linux下的基本一致。