Socket编程实现UDP通信

1.建立服务端

1.1创建套接字(本质是创建了一个文件)

套接字=IP地址+端口号

第一个参数详解

第二个参数详解

socket有指定的类型,该类型确定了通信的语义(规则)

SOCK_STREAM 提供有序、可靠、双向(全双工)、基于连接的字节流。two-way:翻译为全双工,意思是连接一旦建立成功双方可以同时发数据和接收数据;

返回值:

成功返回一个文件描述符fd,失败返回-1;

1.2 绑定

端口号的作用究竟是什么呢?

IP地址可以确定在哪一台机器上,而端口号的作用是确定是哪个程序(进程),所以服务器绑定自己的ip+端口号是为了让客服端精中无误的找到自己这个客户端程序,所以一个端口号只能绑定一个进程,但是一个进程可以绑定多个端口号;

在创建服务器时,bind 绑定的 IP 和端口号,是服务器自身的 IP 和端口 ,而非客户端的。服务器通过 bind 将自身地址注册给操作系统,明确告诉系统 "我要在这个 IP、这个端口上监听客户端请求",让客户端能通过固定地址找到并连接它;而客户端无需主动绑定地址,调用 connect 时操作系统会自动为其分配临时端口和本机 IP,客户端只需知道服务器地址即可主动发起通信,服务器后续能通过客户端发来的数据包自动识别其地址,从而实现双向通信。服务器不会临时分配端口,回复消息依然使用 bind 绑定的固定端口;只有客户端不 bind,由系统自动分配临时端口。

客户端的 IP 地址由本机网卡决定,通信过程中始终固定不变 ;端口则由操作系统自动随机动态分配,且分配规则由连接状态与目标决定,因为客户端并不需要被找到,只需要具有唯一性即可,并且客户端如果自己绑定端口号很容易造成匹配冲突,使得客户端启动不起来,与同一服务器建立长连接时,客户端在连接生命周期内只会使用同一个临时端口,直到连接断开才会被系统回收;与不同服务器通信时,操作系统会为每条新连接的客户端分配独立的临时端口,以保证四元组(源 IP + 源端口 + 目标 IP + 目标端口)不冲突,实现多进程、多连接的并行通信;即使断开后重连同一服务器,系统也大概率会分配新的临时端口,而非强制复用旧端口。

绑定成功返回0,否则返回1,并设置错误码;

值得注意的是不同CPU存储数字的方式并不完全一样,我们必须要将我们的Ip+port(端口)转成大家都能认识的字节序,不然对方很有可能根本读不懂我们的IP+port;而世界上只有两种CPU,一种是小端序,一种是大端序,绝大多数的电脑使用的是小端序,即低地址放在前面,少数系统和网络设备使用的是大端序,即高地址放在前面,为了防止冲突发生,网络规定全世界使用一种格式即网络字节序,也就是大端序,无论任何设备,手机,MAC,window,linux,在发送数据的时候都要转成网络字节序(大端序),接收收据之后都要转成主机字节序(小端序);以下是对应的API

而我们在写IP的时候由于IP是数字加字符的格式,所以在初始化的时候我们只能使用string(char *),但是网络中需要的uint32_t,所以我们得使用Linux给我们已经定义好的函数来进行转换

我们发现sin_addr底层是struct,结构体只能整体初始化,不能赋值

1.3发送和接收数据

1.3.1API

注意:recvfrom的struct * src_addr 和socklen_t * addrlen是输入形参数;

1.3.2 netstat

netstat是Linux系统的网络查看工具,主要用于查看本机端口占用情况、TCP/UDP连接状态、服务监听状态与网络统计数据,常用来排查bind失败、端口冲突、服务未正常监听等网络编程问题,辅助快速定位网络程序运行异常。

bash 复制代码
# 查看所有TCP监听端口(最常用)
netstat -lt

# 查看所有TCP+UDP端口
netstat -lut

# 显示端口号+进程PID
netstat -ltp

# 查看所有连接,包含TIME_WAIT
netstat -at

2.建立客户端

客户端必须手动提前知道 / 写死服务器的 IP 与端口号,才能主动发起 connect 连接;服务器提前通过 bind 绑定固定端口并监听,IP 一般填本机地址或公网地址,不会主动把自身 IP、端口发给客户端,而是被动等待连接,客户端依靠预先配置的服务端地址信息,精准定位并访问服务端进程。

值得注意的是客户端绑定的服务器不一定只有一个;

3.服务器是否应该绑定确定的IP?

4.建立UDP通信的服务器端实现

cpp 复制代码
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__

#include <iostream>
#include <memory>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <functional>
#include "log.hpp"
#include "conmmen.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string&)>;
const static int gsocketfd =-1;
const static std::string gdefaultIp="127.0.0.1";             //表示你当前正在用的主机ip,也叫本地环回地址,通常用于做代码测试
const static uint16_t gdefaultport=8081;
class UdpServer
{
    public:
    UdpServer(func_t func,uint16_t port=gdefaultport)
    :_sockfd(gsocketfd)
    ,_func(func)
    ,_addr(port)
    ,_isrunning(false)
    {}

    void initUdpServer()
    {
        //1.创建套接字
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(loglevel::FATAL)<<"socket:"<<strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(loglevel::INFO)<<"创建socket成功,socket:"<<_sockfd;


        //2.绑定端口号和IP
        //2.1填充端口号和IP信息
        // struct sockaddr_in local;                                      //sockaddr_in 是 Linux 网络编程里定义 IPv4 地址的结构体
        // local.sin_family=AF_INET;
        // local.sin_port= htons(_port);
        // local.sin_addr.s_addr= INADDR_ANY;
        //2.2绑定  -> 设置到内核中
        int n = bind(_sockfd,_addr.ofNetAddr(),_addr.ofNetAddrLen());
        if( n < 0)
        {
            LOG(loglevel::FATAL)<<"bind:"<<strerror(errno);
            Die(BIND_ERR);
        }
        LOG(loglevel::INFO)<<"绑定成功";
    }
    void start()
    {
        _isrunning=true;
        while(1)
        {
            char inbuffer[1024];  //输入缓冲区
            struct sockaddr_in  peer;
            socklen_t len = sizeof(peer);   
            ssize_t n= recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,cast(&peer),&len);   //&len这个接受形参数是为了确定接收回来的缓冲区的大小,输出形参数,所以必须用指针
            // uint16_t clientport=::ntohs(peer.sin_port);
            // std::string clientip=::inet_ntoa(peer.sin_addr);
            InetAddr client(peer);
            //接收成功返回的是接收数据的字节数
            if(n>0)
            {
                inbuffer[n]=0;    //清空缓冲区
                std::string clientinfo = client.Ip() + ":"+ std::to_string(client.Port())+inbuffer;
                LOG(loglevel::DEBUG)<<clientinfo;;
                //std::cout<<clientport<<clinetip<<std::endl;
                // std::string echo_string="echo#";
                // std::string message;
                // std::getline(std::cin,message);
                // echo_string += message;
                std::string result = _func(inbuffer);
                ::sendto(_sockfd,result.c_str(),result.size(),0,cast(&peer),sizeof(peer));
            }
        }
        _isrunning =false;
    }
    ~UdpServer()
    {
        if(_sockfd > gsocketfd)
        {
            ::close(_sockfd);
        }
    }
    
    private:
    int _sockfd;
    uint16_t _port;
    std::string _ip;
    bool _isrunning;
    InetAddr _addr;
    func_t _func;

};

#endif
相关推荐
A小辣椒3 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒7 小时前
TShark:基础知识
linux
AlfredZhao9 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言