Socket 套接字的学习--UDP

发布于:2025-08-14 ⋅ 阅读:(14) ⋅ 点赞:(0)

上次我们大概介绍了一些关于网络的基础知识,这次我们利用编程来深入学习一下

一:套接字Socket

1.1什么是Socket

socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,IPv4IPv6,. 然而, 各种网络协议的地址格式并不相同。
1.2套接字的分类
套接字变成socket 的种类会变得多一些
<1> 本地socket (unix 域间socket)- > sockaddr_un
<2>网络socket :本地加网络 - > 用来通信 - > sockaddr_in
<3>最后都统一成了一个套接字,这个套接字作为统一的接口 ,sockaddr
为什么要统一接口呢?如果不同的厂商使用不同的网络接口,那么任何进程或者是客户端服务器之间的通信,将会十分麻烦,所以OS 提供了系统调用。所以使用本地或者网络的时候,要设置它的AF_INET还是AF_UNIX,也就是图片中的前十六位。然后设置完一些内部的类型等之后,最后都要强转成sockaddr这个类型的接口,去使用
!c语言中经常使用一些void* ,然后作强转,那为什么这没有用void* 最后去强转成想要的类型,而是直接使用了 这个sockaddr 这个结构体?
当时研究出来网络接口的时候,c语言还没有 void * ,其次就是 使用这个 其他类型,因为内部结构可以看出其实是很像c++里面的继承与多态,这样的话关联关系明显。
:UDP 服务通信
当然,通信的话肯定是两者以上在通信,我们今天用一个客户端和一个服务端作为通信的双方,然后使用UDP 协议进行 云平台之间的网络通信 和 云平台与Windows下的网络通信。
我们先梳理一下程序流程:
1.1 服务端流程
首先按照我们日常写的类的话,肯定就是先要初始化一些接口(网络通信的接口)
其次初始化接口结束后,我们作为服务端,就要等待客户端的一些需求,我们今天的任务是简单的通信交流,所以就是等待客户端的消息,然后收到消息之后就要回话,要知道是谁给我们发的消息,回话的话是给谁发,然后接着等待以后就重复就行了
1.2客户端流程
作为客户端,我们要知道给谁发消息,然后等待接受对方回过来的消息,接着重复就好了。细节我们慢慢了解。
1.3 认识创建套接字函数 --socket 
首先呢,我们明确一下头文件,在学习网络的过程中,有四个头文件每次都要包
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

socket 

我们可以用man 直接查这个socket,第一个参数是域或者协议家族,什么意思呢,就是以下这些

看它第二列目的,抵押给是本地通信,第二哥就是域间通信,第三个是IPV4,我们今天就用IPv4规定我们的IP地址。

第二个参数是代表套接字类型,也就是我们使用的TCP协议,还是UDP协议,我们今天用的UDP协议

UDP协议是不连接的,不可靠的,有最大长度的,所以就是 SOCK_DGRAM
第三个参数 默认为 0 就可以了
我们看一下这个函数的返回值
它说这个函数成功,就会返回一个套接字的文件描述符,其实也就是文件描述符,只不过是在网络中的一个文件,相当于你要是想要在网络中通信,就必须要在这个文件里写或者读。
LOG是我封装了一个日志,以后会更新这个的。
1.4 bind--绑定网络信息
我们说了socket 只是给我们创建了一个网络上通信用的文件,接着我们就要对于这个文件绑定对应的网络信息,什么信息呢,你要发送,是不是要告诉对方你是谁,对方才能给你回消息。
我们看这个bind,第一个参数不就是我们刚才用socket创建好的套接字返回的描述符吗
第二个参数 ,struct sockaddr* 这个结构体指针代表者什么呢

这个是什么呢,##这个符号,叫做拼接符,什么啥意思呢,就是前面的参数拼接我后面的family,也就是现在它现在传的这个参数是sa_ ,那么拼接起来就是sa_family。

也就是sa_family 代表 用AF_INET 等类型初始化

但是我们刚才也说了sockaddr ,是作为最后的网络的统一的接口的,也就是说,我们在UDP通信的时候今天申请的是网络通信,所以用的是sockaddr_in,所以我们要用sockaddr_in这个结构体把网络信息存进去,最后强转成sockaddr。

我们看这个结构体,第一个就是 sin_family(刚才说的拼接)

第二个 sin_port ->端口号

第三个 sin_addr -> IP地址

所以我们要把我们这个服务端的协议族,端口号,IP地址存进去。

 struct sockaddr_in local;
 bzero(&local, sizeof(local)); // 最好是先清零再去给它赋值 用memset也可以
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port); // 因为字节序的问题 ,最好是要给转换成网络的大端
        local.sin_addr.s_addr = inet_addr(_ip.c_str());

当大家写的时候会发现,并不能直接将端口号直接赋值给sin_port,因为什么呢?因为我们上次说了网络中也是存在大小端的,就是有字节序的问题,所以我们要先转换成大端,也就是用htons函数

也不能直接把IP地址直接赋值给sin_addr,为什么呢?因为sin_addr它本身是一个结构体,我们还要在结构体里面找结构体成员进行赋值。

,因为我申请的IP用的是string 存放的,所以我想把这个村给s_addr(类型是32位4字节),就要转换,用inet_addr()把char*转换成uint32_t 。

把网络信息存好,就可以进行绑定了。

//int n = bind(_sockfd, CONV(&local), sizeof(local));
        
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";

1.5 接受网络消息--recvfrom

因为我们的UDP的传输报文方式是数据报形式的,所以用recvfrom

第一个参数依旧是老朋友,文教描述符,

第二个是一个void* buffer,意思就是存放信息的地方,

第三个是长度,

第四个默认位0就行,

第五个依旧就是统一接口sockaddr,代表发送端的网络信息(IP+Port)

第六个的socklen_t 代表sockaddr 的大小是多大的,用一个socklen_t的变量存起来然后取地址就可以。

 ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);

1.6 sendto 发送消息

依旧是因为UDP传输的数据报,选择sendto

第一个参数依旧是老朋友,文教描述符,

第二个是一个void* buffer,意思就是存放信息的地方,

第三个是长度,

第四个默认位0就行,

第五个依旧就是统一接口sockaddr,代表是发送端的网络信息

第六个的是sockaddr的大小,直接用sizeof就可以

然后我们处理一下接受到的消息,之后再给客户端发送过去即可。

void Start()
    {
        LOG(LogLevel::INFO) << "start";
       
        while(true)
        {
            //要接受客户端的信息
            //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                            struct sockaddr *src_addr,  *addrlen);

            char inbuffer[1024];
            struct sockaddr_in peer;
           
            socklen_t len = sizeof(peer) ;
            ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);

            if(n>0)
            {
                uint16_t clientport = ::ntohs(peer.sin_port);
                std::string clientip = ::inet_ntoa(peer.sin_addr);
                

                inbuffer[n] = 0;

                std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;
                
                LOG(LogLevel::DEBUG) << clientinfo;
                //接受消息后 还有给客户端发送数据
                std::string echo_string = "echo#" ; 
                echo_string += inbuffer;
                
                ::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);

            }
            else
            {
                 LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);
            }
            

        }

我们再说一个转换函数,这个要比inet_ntoa()函数转换更加安全

_ip = ::inet_ntop(AF_INET, &_local.sin_addr, ipbuffer, sizeof(ipbuffer));

inet_ntoa 本质呢是在内部有一个静态存储区,如果是多线程调用它,很容易进行覆盖,所以它并不安全,inet_ntop 这个函数是让你自己申请一个静态区,所以由程序员自己掌握,相对安全。

1.6 是否要绑定固定的 IP 和端口号

端口号有一定的定义范围

IP地址的定义范围


IPv4地址是一个32位的二进制数,通常被分为四段表示,每段8位,并以点分十进制(dotted-decimal notation)的形式表示,即每个字节转换为对应的十进制数字,各字节间用点号隔开。范围:0.0.0.0~255.255.255.255
特殊用途的IPv4地址范围包括:
私有地址:这些地址范围专门保留用于内部网络,不会在全球互联网路由中出现。10.0.0.0 ~ 10.255.255.255    172.16.0.0 ~ 172.31.255.255     192.168.0.0 ~ 192.168.255.255
回环地址:用于本机回环测试,通常使用127.0.0.1代表本地主机。
127.0.0.0 到 127.255.255.255
自动专用IP寻址:当无法从DHCP服务器获得配置时,系统可能会自动配置此范围内的地址。
169.254.0.0 到 169.254.255.255。

我们要不要再服务器中,把某个进程也就是它的端口号,和IP绑定在一起呢

答案是,不要,因为一个公司里的服务器,会有很多的IP地址,我们要给同一个端口发送消息,那么经过不同的IP,同一个端口号,都能给这个进程发送信息,但是如果你绑定了特定的IP,那么只有通过这个IP,才能给这个进程发送消息,所以我们不能绑定IP。

1.7 客户端

在我们学习完服务端之后,客户端就很简单了。

首先依旧是创建一个网络通信的文件,和上面一样。

然后区别来了:客户端用不用绑定网络信息呢??、

答案是:不用。为什么呢?难道客户端发送不需要对方知道自己的网络信息吗?并不是这样的,只是不用绑定,是因为系统回自动给客户端自由随机分配一个,为什呢?假设你的淘宝进程 你给绑定了80的端口号,而抖音也要这个端口号,那么不就起了冲突吗?而让系统自己分配,就可以避免这种冲突。只是不用手动绑定,而不是没有网络信息。

接着是一样的发送消息,接受消息。

这个的设计思路是,直接默认手输服务端的IP和端口,所以我们要用 argc 和 argv 两个参数。

 if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG();
    // ip + port
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    struct sockaddr_in tmp;
    memset(&tmp, 0, sizeof(tmp));
    tmp.sin_family = AF_INET;
    tmp.sin_port=::htons(serverport);
    tmp.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::cout<<"输入你要说的话"<<std::endl;
        std::string tstring;
        std::getline(std::cin,tstring);

        int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"Client send to false";
        }

        char inbuffer[1024];
        struct sockaddr_in server;
        socklen_t len = sizeof(server);
        int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);
        if(m>0)
        {
            inbuffer[m] = 0;
            std::cout<<inbuffer<<std::endl;
        }

    }

1.8 实现云平台和云平台直接的通信

那么你直接开两个终端,一个运行客户端,一个运行服务端,默认服务端 的端口号8080或者8888都可以。然后直接通信就可以了

1.9 云平台和Windows 的通信

首先你要在你的云平台的管理器那里,申请UDP通信,要保证你的UDP端口是可以用的。接下来给大家分享一个Windows端作为用户端的代码,云平台依旧使用我们刚才的服务端的代码就可以

#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 = 8080;//默认一个端口号就可以
int main()
{
	WSADATA wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);
	struct sockaddr_in server;
	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());
	SOCKET 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 Enter@ ";
		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;
}

网络的代码一样,只是在Windows下,某些文件需要提前配置和打开关闭。

2.0奉上全部代码

UDPClient.cc

#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG();
    // ip + port
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        Die(SOCKET_ERR);
    }
    struct sockaddr_in tmp;
    memset(&tmp, 0, sizeof(tmp));
    tmp.sin_family = AF_INET;
    tmp.sin_port=::htons(serverport);
    tmp.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::cout<<"输入你要说的话"<<std::endl;
        std::string tstring;
        std::getline(std::cin,tstring);

        int n = ::sendto(sockfd,tstring.c_str(),tstring.size(),0,CONV(&tmp),sizeof(tmp));
        if(n<0)
        {
            LOG(LogLevel::FATAL)<<"Client send to false";
        }

        char inbuffer[1024];
        struct sockaddr_in server;
        socklen_t len = sizeof(server);
        int m = ::recvfrom(sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&server),&len);
        if(m>0)
        {
            inbuffer[m] = 0;
            std::cout<<inbuffer<<std::endl;
        }

    }
  

    return 0;
}



UDPServer.hpp

#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static  std::string fault_ip = "127.0.0.1";
const static  uint16_t fault_port = 8080;
class UDPServer
{
public:
    UDPServer(std::string ip = fault_ip, uint16_t port = fault_port)
        : _ip(ip), _port(port), _isrunning(false), _sockfd(-1)
    {
    }
    void InitServer()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); //这是创建好了一个网络上的文件
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket create false";
            Die(USAGE_ERR);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 最好是先清零再去给它赋值 用memset也可以
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port); // 因为字节序的问题 ,最好是要给转换成网络的大端
        local.sin_addr.s_addr = inet_addr(_ip.c_str());


        //要绑定网络上的信息  你要知道是谁传的 ip + port
        int n = bind(_sockfd, CONV(&local), sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Start()
    {
        LOG(LogLevel::INFO) << "start";
        _isrunning = true;
        while(true)
        {
            //要接受客户端的信息
            // ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
            //                   struct sockaddr *src_addr,  *addrlen);

            char inbuffer[1024];
            struct sockaddr_in peer;
           
            socklen_t len = sizeof(peer) ;
            ssize_t n = ::recvfrom ( _sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);

            if(n>0)
            {
                uint16_t clientport = ::ntohs(peer.sin_port);
                std::string clientip = ::inet_ntoa(peer.sin_addr);
                inbuffer[n] = 0;

                std::string clientinfo = clientip + ":" + std::to_string(clientport) + "#" + inbuffer;
                LOG(LogLevel::DEBUG) << clientinfo;
                //接受消息后 还有给客户端发送数据
                std::string echo_string = "echo#" ; 
                echo_string += inbuffer;
                
                ::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,CONV(&peer),len);

            }
            else
            {
                 LOG(LogLevel::FATAL) << "recvfrom: " << strerror(errno);
            }
            

        }

    }
    ~UDPServer()
    {
    }

private:
    int _sockfd; // 建立的网络文件描述符
    std::string _ip;
    uint16_t _port;
    bool _isrunning;
};

#endif

UDPServer.cc

#include<iostream>
#include "Log.hpp"
#include "UdpServer.hpp"
#include <string>
#include "Common.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
        Die(USAGE_ERR);
    }

    ENABLE_CONSOLE_LOG();
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);



    std::unique_ptr<UDPServer> svr_uptr = std::make_unique<UDPServer>(ip,port);
    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

Common.hpp

#pragma once

#include <iostream>


#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum 
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};


网站公告

今日签到

点亮在社区的每一天
去签到