UDP协议开发

2022-09-02 11:29:25 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。


1 简介

在进行电网插件开发的过程中,对电网接入程序进行了开发,使得在综合安防管理平台上能够非常方便的接入天地维正电网设备。电网数据采用UDP协议,通过监狱局域网,向用户指定的5个IP地址的某端口,同时发送,各IP地址收到的数据相同。因为是第一次使用网络数据报进行开发,因此遇到了许多的坑。在这里把遇到的问题组织成一个文档,重新理解在代码撰写过程中遇到的问题。本文档适用于初次使用UDP进行开发的人员,测试人员也可以阅读使用。


2 UDP是什么

2.1 OSI

OSI全称Open System InterConnect, 开放系统互联,是国际标准化组织在1985年研究的网络互联模型。该体系结构定义了网络互联的七层框架,[物理层-数据链路层-网络层-传输层-会话层-表示层-应用层]即ISO开放系统互联模型。 TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接口层。常见的网络协议和所属层次可参见下表:

UDP用户数据报[User Datagram Protocol]属于传输层,传输层最重要的两个数据报协议为TCP协议和UDP协议。 开放系统互联模型每一层较为具体的功能可以参考下图:

2.2 UDP格式

UDP是一种简单的,面向数据报的无连接的协议,提供的是一种不可靠的协议,就如同真实世界中的短信功能,虽然我们用手机编辑了短信,并且也点击发送,但短信的内容是否真的能够发送到目标手机,是未知的,由于网络原因或者其他的原因,短信可能没有达到目标手机,但对于发送方来说,确实发送了短信。UDP数据报分为首部和数据两个部分,格式如图所示:

由上图可以看出,UDP是在IP协议的基础上增加了新的内容,即源端口,目的端口,长度和校验和。

2.3 UDP的功能

TCP协议复杂,但传输可靠。UDP协议简单,但传输不可靠。UDP相对于TCP较为简单,网络层IP协议是一种不可靠的传输,在TCP/TP协议族中,它只是尽可能快地把分组从源节点送到目的节点,但不提供任何可靠性的保证。 Tcp在不可靠的ip层上,提供了一个可靠的运输层,为了提供这种可靠的服务,TCP采用了超时重传、发送和接受端到端的确认分组等机制。 UDP的传输与IP协议非常相似,UDP协议同样以数据包(datagram)的方式传输,它的传输方式也是”Best Effort”的,所以UDP协议也是不可靠的(unreliable)。UDP是无连接的,通信双方不需要建立物理链路连接。在网络中它用于处理数据包。那么,我们为什么不直接使用IP协议而要额外增加一个UDP协议呢? 一个重要的原因是IP协议中并没有端口(port)的概念。IP协议进行的是IP地址到IP地址的传输,这意味者两台计算机之间的对话。但每台计算机中需要有多个通信通道,并将多个通信通道分配给不同的进程使用。与TCP一样,处理数据报,但是是一种无连接的方式。因此较TCP而言不可靠。 在选择使用协议的时候,选择UDP必须要谨慎。在网络质量令人十分不满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如我们聊天用的ICQ和QQ就是使用的UDP协议。 UDP在IP协议的基础上添加了源端口,目标端口号,数据报长度,校验值等基本内容组成。

2.4 UDP工作流程

进入传输层之后,我们也可以调用操作系统中的API,来构建Socket。Socket是操作系统提供的一个编程接口,它用来代表某个网络通信。应用程序通过socket来调用系统内核中处理网络协议的模块,而这些内核模块会负责具体的网络协议的实施。这样,我们可以让内核来接收网络协议的细节,而我们只需要提供所要传输的内容就可以了,内核会帮我们控制格式,并进一步向底层封装。因此,在实际应用中,我们并不需要知道具体怎么构成一个UDP包,而只需要提供相关信息(比如IP地址,比如端口号,比如所要传输的信息),操作系统内核会在传输之前会根据我们提供的相关信息构成一个合格的UDP包(以及下层的包和帧)。

上图以一种非常清晰的图式说明UDP的工作流程,UDP以C/S的模式工作,因此在客户端和服务器端需要建立socket对象,由两个socket对象完成数据的传输和接收。上图为UDP服务端和客户端的套接字函数。


3 UDP和TCP协议的区别

1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接 2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付 3、Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。 3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。 4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信 5、TCP对系统资源要求较多,UDP对系统资源要求较少。


4 何时用到UDP

UDP以其简单、传输快的优势,在越来越多场景下取代了TCP,如实时游戏。 (1)网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。 (2)TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于TCP内置的系统协议栈中,极难对其进行改进。 采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。


5 电网协议

数据包 CA 1B 10 00 00 01 52 03 16 00 01 01 51 03 15 00 02 01 53 03 14 00 03 01 52 03 15 00 DD 含义: CA—包头,1B—发送27字节数,10—第1分监狱电网,00—保留 00 01 52 03 16 00—防区1,状态正常,电压8500V,电流22MA。 01 01 51 03 15 00—防区2,状态正常,电压8490V,电流21MA。 02 01 53 03 14 00—防区3,状态正常,电压8510V,电流20MA。 03 01 52 03 15 00—防区4,状态正常,电压8500V,电流21MA。 DD—累加和低字节。 电网的事件一般有01-正常,02-断网报警,03-触网报警,04-短路报警。开发的目的就是在平台上可以对接电网设备,这样在设备向平台发送了类似上述数据报的时候,平台可以从数据报中解析出电压,电流和上报事件。

5.1 UDP开发基本思路

因为开发的目的是设备以UDP的方式向平台发送数据报,因此在程序中应该有一个线程不停的接收数据,然后接收到数据之后处理数据,在数据报中保存电压电流值并且在有事件时,上报给平台。因为是UDP通讯,所以在程序中要建立一个UDP Socket,设置Server Socket的属性,不断的接收UDP数据报。然后验证接收到的数据报的有效性,并在程序中解析数据报。

5.2 协议开发

Protocol协议是公共的插件协议,平台能够介入的设备协议继承自该类,然后把数据解析的过程在代码里完成即可。在下述的代码片段中,比较重要的设计就是GetAlarmEventPro()函数设计成了类TDWZProtocol协议类的友元。这么设计是因为在需要访问该类型中的私有成员,诸如m_bConnected, bStop4Pro比较方便。该函数为线程的执行体函数,会不断的接收数据。而DealWithData则是该类的辅助功能函数,负责处理接收到的合法数据。 该类型的大体定义如下:

代码语言:javascript复制
class TDWZProtocol : public Protocol
{
public:
    TDWZProtocol();
    virtual ~TDWZProtocol();

    friend HPR_VOIDPTR CALLBACK GetAlarmEventPro(void* param);

    virtual bool Init();
    virtual void UnInit();
    virtual bool Connect(const std::string& ip, uint16_t port, 
                         const std::string& usrName, const std::string& pwd, const std::string& /*cfgInfo*/);
    virtual bool DisConnect();
    virtual void SetEventNotify(PEventNotify pNotify) { m_pNotify = pNotify; }

private:
    void DealWithData(const unsigned char *ucBuffer, unsigned int len);
private:
    //创建线程,循环接收UDP数据报
    HPR_HANDLE m_hRecvThread;
    //socket通信,用于接收UDP数据报
    HPR_SOCK_T m_sock;
    volatile bool m_bStop4Pro;
    uint16_t    m_iPort;//端口
    HPR_ADDR_T  m_addrLocal;
    PEventNotify m_pNotify;
    bool m_bConnceted;

};

5.3 Socket

Socket的建立主要是为了通信,而且为了接受设备发送过来的数据,需要监听指定的IP和端口,因为UDP实现的端口到端口的通信,因此socket的通信中需要指定端口和IP。

代码语言:javascript复制
bool TDWZProtocol::Init()
{
    DRV_LOG_TRACE("- TDWZProtocol::Init Starts");
    //创建Socket, m_sock是文件描述符
    this->m_sock = HPR_CreateSocket(AF_INET, SOCK_DGRAM, 0);
    bool ret = true;
    do 
    {
        if (HPR_INVALID_SOCKET == m_sock)
        {
            DRV_LOG_ERROR("[0xx] - [HPR_CreateSocket.errorcode=0xx]HPR_CreateSocket fails", DRV_INIT_FAILED, HPR_GetLastError());

            ret = false;
            break;
        }
        HPR_MakeAddrByString(AF_INET, ADDR_ANY, (HPR_INT16)m_iPort, &m_addrLocal);
        //把Socket绑定到指定的端口,其中m_addrLocal保存了IP和端口信息
        if (HPR_Bind(m_sock,&m_addrLocal) < 0 )   
        {
            HPR_CloseSocket(m_sock);
            m_sock = HPR_INVALID_SOCKET;
            DRV_LOG_ERROR("[0xx] - [HPR_Bind.errorcode=0xx]HPR_Bind fails",DRV_INIT_FAILED, HPR_GetLastError());
            ret = false;
            break;
        }

        HPR_UINT32 iMode = 1;

        if (SOCKET_ERROR == HPR_Ioctl(m_sock, (long)FIONBIO, &iMode)) /* 设置为非阻塞模式*/
        {
            HPR_CloseSocket(m_sock);
            m_sock = HPR_INVALID_SOCKET;
            DRV_LOG_ERROR("[0xx] - [HPR_Ioctl.errorcode=0xx]HPR_Ioctl fails",DRV_INIT_FAILED, HPR_GetLastError());
            ret = false;
            break;
        }
        //设置为阻塞模式
        if(SOCKET_ERROR == HPR_SetNonBlock(m_sock,HPR_FALSE))
        {
            HPR_CloseSocket(m_sock);
            m_sock = HPR_INVALID_SOCKET;
            DRV_LOG_ERROR("[0xx] - [HPR_SetNonBlock.errorcode=0xx]HPR_SetNonBlock fails",DRV_INIT_FAILED, HPR_GetLastError());
            ret = false;
            break;
        }
        //设置超时时间,发送不设置超时,接收超时为秒
        if (-1 == HPR_SetTimeOut(m_sock, 0, 1000))
        {   
            HPR_CloseSocket(m_sock);
            m_sock = HPR_INVALID_SOCKET;
            DRV_LOG_ERROR("[0xx] - [HPR_SetTimeOut.errorcode=0xx]HPR_SetTimeOut fails",DRV_INIT_FAILED, HPR_GetLastError());
            ret = false;
            break;
        }
        ret = true;

    } while (0);
    DRV_LOG_TRACE("- TDWZProtocol::Init Finishes");
    return true;
}

在代码里最后的设置超时是必要的,因为UDP在接收数据时,因为之前设置了阻塞模式,因此在接收不到数据时,会一直阻塞在接收函数哪里,通过设置接收超时,比如说1秒,在1秒内超时,说明没有接收到数据,程序继续往下执行,去判断程序的其他逻辑,而不会卡死在recv处。

5.4 线程

另外比较重要的函数是Connect函数,负责与设备建立联系。该函数体如下:

代码语言:javascript复制
bool TDWZProtocol::Connect(const std::string& ip, uint16_t port, const std::string& usrName, const std::string& pwd, const std::string& /*cfgInfo*/)
{
    DRV_LOG_TRACE("- Connect Execute start");
    DRV_LOG_DEBUG("- Connect ip : %s, port : %d", ip.c_str(), port);
    m_iPort = port;
    bool bRet = false;
    do 
    {
        if (true == Init())
        {
            m_bStop4Pro = false;
            //创建线程
            if (!m_hRecvThread)
            {
                m_hRecvThread = HPR_Thread_Create(GetAlarmEventPro, (void*)this, 0);
            }
            if (!m_hRecvThread)
            {
                DRV_LOG_ERROR("[0xx] - [HPR_Thread_Create.errorcode=0xx]HPR_Thread_Create fails", DRV_ERR_CONNECT_FAILED, HPR_GetLastError());
                bRet = false;
                break;
            }
             bRet = true;
        }else
        {
            DRV_LOG_ERROR("- [Init().errorcode=0xx]", DRV_INIT_FAILED);
            bRet = false;
        }

    } while (0);

    if (bRet)
    {
        m_bConnceted = true;
    }

    DRV_LOG_TRACE("- Connect Finishes");
    return m_bConnceted;
}

在Connect函数先执行了Init函数完成Socket函数的建立,然后通过HPR_Thread_Create函数建立线程,并且把GetAlarmEventPro函数作为函数的线程体,当前对象this指针传给了GetAlarmEventPro函数。GetAlarmEventPro函数的函数体如下:

代码语言:javascript复制
HPR_VOIDPTR CALLBACK GetAlarmEventPro(void* param)
{
    DRV_LOG_TRACE("- GetAlarmEventPro Starts");
    TDWZProtocol* pDwDevice = (TDWZProtocol*) param;
    int iRecvSize = -1;
    //ucBuffer数组接收数据报。
    unsigned char ucBuffer[HUN_PKG_DATA_MAX] = {
  
  0};
    HPR_ADDR_T remoteAddr; 
    int nDefenceNo = -1; //防区号
    //因为是友元,所以可以访问类TDWZProtocol的私有数据
    while(!pDwDevice->m_bStop4Pro) 
    {
        if (!pDwDevice->m_bConnceted)//未连接
        {
            Sleep(100);
            continue;
        }
        HPR_ZeroMemory((HPR_VOIDPTR)&ucBuffer, sizeof(ucBuffer));
            //接收数据报的内容,并且放置在数组ucBuffer中
        iRecvSize = HPR_RecvFrom(pDwDevice->m_sock, ucBuffer, sizeof(ucBuffer)-1, &remoteAddr);
        //Here,we can get the remote Ip from remoteAddr knowing that the UDP package's source
        if (iRecvSize > 0)
        {

            DRV_LOG_TRACE("- Receive an UDP Package");
            if (ucBuffer[0] == 0xCA)
            {
                //验证数据报长度有效性
                unsigned int length = ucBuffer[1] 2;
                if (iRecvSize != length)
                {
                    DRV_LOG_ERROR("- [HPR_RecvFrom.errorcode=0xx]UDP Package Received Has Wrong Length", DRV_ERR_INVALID_ARG);
                    continue;
                }
                int CheckSum = 0;
                for (int i=1; i!=length-1; i  )
                {
                    CheckSum  = ucBuffer[i];
                }
                //S1累加和,字节;除Q1外的上述全部字节相加,取相加和的低字节
                BYTE sum = CheckSum & 0xFF;
                if (sum == ucBuffer[length-1])
                {
                    DRV_LOG_TRACE("- Program gets a valid UDP package, Ready to Process");
                    /*负责处理接收到的数据报,因为ucBuffer是一个字节数组,并不是以结尾,所以需要传入字符数组的长度*/
                    pDwDevice->DealWithData(ucBuffer, length);  
                }else
                {
                    DRV_LOG_ERROR("- [HPR_RecvFrom.errorcode=0xx]Invalid UDP package, checkSum fails.", DRV_ERR_INVALID_ARG);
                    continue;
                }

            }else
            {
                DRV_LOG_ERROR("- [HPR_RecvFrom.errorcode=0xx]UDP package Header invalid", DRV_ERR_INVALID_ARG);
                continue;
            }

        }else
        {

            HPR_UINT32 iErrNo = HPR_GetSystemLastError();
            if (iRecvSize==0)
            {
                DRV_LOG_TRACE("- Socket is closed");
                //HPR_CloseSocket(pDwDevice->m_sock);
                /* if (pDwDevice->m_pNotify) { //通知设备离线事件的发生 pDwDevice->m_pNotify->OnEventOffline(); } */    
            }
           //TODO 上报,断开连接重新connect

        }

    }

    DRV_LOG_TRACE("- GetAlarmEventPro Finishes");
    return NULL;
}

5.5 UDP数据报解析

UDP数据报的解析过程主要在DealWithData中进行处理,主要的代码逻辑如下:

代码语言:javascript复制
void TDWZProtocol::DealWithData(const unsigned char* ucBuffer, unsigned int len)
{
    DRV_LOG_TRACE("- TDWZProtocol::DealWithData Starts");
    //数据报中含有的防区数目,表示数据报中有几组防区数据
    unsigned int DefenceAreaNo = (ucBuffer[1]-1)/6;
    unsigned int PowerGridNo = ucBuffer[2]/0x10;

    DRV_LOG_DEBUG("- Power Grid No is %d", PowerGridNo);
    //防区号
    unsigned int nDefenceNo;
    for (unsigned int i=0; i<DefenceAreaNo; i  )
    {
        DRV_LOG_DEBUG("- DefenceNo=%d, status=%x", ucBuffer[4 i*6] 1, ucBuffer[5 i*6]);
        //编译器隐式执行的任何类型转换都可以使用static_cast显式完成
        nDefenceNo = static_cast<unsigned int>(ucBuffer[4 i*6]) 1;
        Event e;
        std::string DefenceAreaId = CommonTools::Int2Str(nDefenceNo);
        e.happenTime = CommonTools::Time2DateTimeStr(time(0));
        e.eventId = CommonTools::NewGuid();
        //瞬时事件
        e.status = 0;
        //channelIdentity表示防区号。第一防区为“”
        e.channelIdentify = DefenceAreaId;

        //防区事件发生
        switch(ucBuffer[5 i*6])
        {
        case 0x01:
            //正常
            e.eventType = EVENT_ELECNET_NORMAL;
            break;
        case 0x02:
            //02-断网报警
            e.eventType = EVENT_ELECNET_WIREBREAK;
            DRV_LOG_DEBUG("- Defence Area %s : Status WireBreak", e.channelIdentify.c_str());
            break;
            //03-触网报警,

        case 0x03:
            e.eventType = EVENT_ELECNET_TOUCHNET;
            DRV_LOG_DEBUG("- Defence Area %s : Status TouchNet", e.channelIdentify.c_str());
            break;
            //04-短路报警。
        case 0x04:
            e.eventType = EVENT_ELECNET_SHORTOUT;
            DRV_LOG_DEBUG("- Defence Area %s : Status Shortout", e.channelIdentify.c_str());
            break;
            //未定义的防区状态
        default:
            DRV_LOG_DEBUG("- [DefenceAreaStatus.errorcode=0xx]UnDefined Defence Area Status passed", DRV_ERR_INVALID_ARG);
            e.eventType = EVENT_UNCLASSIFIED;
            break;
        }

        if (e.eventType != EVENT_ELECNET_NORMAL)
        {
           DRV_LOG_DEBUG("- Event %d has happened in Defence Area %s", e.eventType, e.channelIdentify.c_str());
           //TODO Program should add the following statements to upload the corresponding events
           if (m_pNotify)
           {
                DRV_LOG_TRACE("- Upload Event");
                m_pNotify->OnEventHappen(e);
           }
        }

        //保存电压电流到m_mapChanExtInfo中

        DRV_LOG_TRACE("- Saving the Voltage and Electric");
        unsigned int voltage;//电压
        unsigned int electric;//电流
        voltage = (ucBuffer[7 i*6]*256   ucBuffer[6 i*6])*10;
        electric = (ucBuffer[9 i*6]*256 ucBuffer[8 i*6]);
        DRV_LOG_DEBUG("- Defence Area %s: Voltage : %d, Electric : %d", DefenceAreaId.c_str(), voltage, electric);
        ExtInfo info;
        info.voltage = voltage;
        info.electric = electric;
        m_mapChanExtInfo[DefenceAreaId] = info;
    }
    DRV_LOG_DEBUG("- TDWZProtocol::DealWithData Finishes");

}

在UnInit函数中负责Socket的关闭,Disconnect函数负责线程的关闭,不在此赘述。至此,UDP数据报的接收,UDP报文有效性验证,UDP数据报解析都可以在代码中简单的查看。 在上述的协议开发中,TDWZProtocol仅作为UDP的Server端,只是在不停的接收设备向平台发送数据,并没有回传给设备数据报文。因此比较简单。但逻辑并不复杂。在开发时,因为并不熟悉代码,而且基于数据报的开发是第一次,所以花费了较多的时间。


6 UDP开发中函数的理解

Window系统为我们进行UDPsocket开发提供了一些常用的函数,简单介绍如下,UDP的框架如下图:

6.1 socket函数

代码语言:javascript复制
#include <sys/types.h> 
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain:用于设置网络通信的域,socket根据这个参数选择信息协议的族

代码语言:javascript复制
Name                                     Purpose                         
AF_UNIX, AF_LOCAL          Local communication              
AF_INET                           IPv4 Internet protocols          //用于IPV4
AF_INET6                         IPv6 Internet protocols          //用于IPV6
AF_IPX                             IPX - Novell protocols
AF_NETLINK                     Kernel user interface device     
AF_X25                            ITU-T X.25 / ISO-8208 protocol   
AF_AX25                          Amateur radio AX.25 protocol
AF_ATMPVC                      Access to raw ATM PVCs
AF_APPLETALK                 AppleTalk                        
AF_PACKET                      Low level packet interface       
AF_ALG                           Interface to kernel crypto API

参数type(只列出最重要的三个):

代码语言:javascript复制
SOCK_STREAM         Provides sequenced, reliable, two-way, connection-based byte streams.   //用于TCP
SOCK_DGRAM          Supports datagrams (connectionless, unreliable messages ). //用于UDP
SOCK_RAW              Provides raw network protocol access.  //RAW类型,用于提供原始网络访问

参数protocol:置0即可 返回值:成功:非负的文件描述符 失败:-1

6.2 sendto函数

代码语言:javascript复制
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得 第二个参数buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据 第三个参数len:发送缓冲区的大小,单位是字节 第四个参数flags:填0即可 第五个参数dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程 第六个参数addrlen:表示第五个参数所指向内容的长度 返回值:成功:返回发送成功的数据长度 失败: -1

6.3 recvfrom函数

代码语言:javascript复制
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得 第二个参数buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据 第三个参数len:接收缓冲区的大小,单位是字节 第四个参数flags:填0即可 第五个参数src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的 第六个参数addrlen:表示第五个参数所指向内容的长度 返回值:成功:返回接收成功的数据长度 失败: -1

6.4 bind函数

代码语言:javascript复制
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得 第二个参数my_addr:需要绑定的IP和端口 第三个参数addrlen:my_addr的结构体的大小 返回值:成功:0 失败:-1

6.5 close函数

代码语言:javascript复制
#include <unistd.h>
int close(int fd);

6.6 setsockopt/getsockopt

代码语言:javascript复制
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level,int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level,int optname, const void *optval, socklen_t optlen);

在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:

代码语言:javascript复制
int nNetTimeout=1000;//1秒
//发送时限
setsockopt (socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt (socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

level指定控制套接字的层次.可以取三种值: 1)SOL_SOCKET:通用套接字选项. 2)IPPROTO_IP:IP选项. 3)IPPROTO_TCP:TCP选项. 对应的optname详细说明 optname指定控制的方式(选项的名称).

选项名称 说明 数据类型

SO_RCVTIMEO 接收超时 struct timeval SO_SNDTIMEO 发送超时 struct timeval


7 实践

7.1 创建UDPServer控制台应用程序

首先编写UDP的服务端,代码如下:

代码语言:javascript复制
//Server
#include<WinSock2.h>
#include<windows.h>
#include<iostream>
using namespace std;

#pragma comment (lib,"ws2_32.lib")
#define PORT 6000
//UDP Server
int main()
{
    //初始化网络环境
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa)!=0)
    {
        cout << "初始化网络环境失败" << endl;
        return -1;
    }
    //创建UDP的socket
    SOCKET sServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sServer==INVALID_SOCKET)
    {
        cout << "创建套接字失败" << endl;
        return -1;
    }
    SOCKADDR_IN si;
    si.sin_family = AF_INET;
    si.sin_port = htons(PORT);
    /*一般情况下,如果你要建立网络服务器,则你要通知服务器操作系统: 请在某地址xxx.xxx.xxx.xxx上的某端口yyyy上进行侦听,并且把侦听到的数据包发送给我。 这个过程,你是通过bind()系统调用完成的。——也就是说,你的程序要绑定服务器的某地址, 或者说:把服务器的某地址上的某端口占为已用。服务器操作系统可以给你这个指定的地址,也可以不给你。*/

  /* 192.168.1.1 202.202.202.202 61.1.2.3 如果你serv.sin_addr.s_addr=inet_addr("192.168.1.1"); 然后监听端口 这时其他机器只有connect 192.168.1.1:100才能成功。 connect 202.202.202.202:100和connect 61.1.2.3:100都会失败。 如果serv.sin_addr.s_addr=htonl(INADDR_ANY); 的话,无论连接哪个ip都可以连上的。*/
    si.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    //开始绑定
    bind(sServer, (sockaddr *)&si, sizeof(sockaddr));
    printf("Now,Server is running on port 6000,waiting for youn");
    char szbuf[2048];
    memset(szbuf, 0, sizeof(szbuf));
    while (TRUE)
    {
        SOCKADDR_IN siClient;
        memset(&siClient, 0, sizeof(sockaddr));
        int siClientLen = sizeof(sockaddr);
        int ret = recvfrom(sServer, szbuf, sizeof(szbuf), 0,(sockaddr *) &siClient, &siClientLen);
        printf("Recv msg:%s from IP[%s],port[%d]n", szbuf, inet_ntoa(siClient.sin_addr),
            ntohs(siClient.sin_port));
        sendto(sServer,"hello,你好",sizeof("hello,你好"),0, (sockaddr *)&siClient, siClientLen);
        printf("send back to IP[%s],port[%d]n",  inet_ntoa(siClient.sin_addr),
            ntohs(siClient.sin_port));
    }
    return 0;
}

7.2 创建UDPClient控制台应用程序

创建UDPClient应用程序,与UDPServer程序进行数据交互,代码如下:

代码语言:javascript复制
//Client

#include<WinSock2.h>
#include<windows.h>
#include<iostream>
using namespace std;

#pragma comment (lib,"ws2_32.lib")
#define PORT 6000
//UDP Server
int main()
{
    //初始化网络环境
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        cout << "初始化网络环境失败" << endl;
        return -1;
    }
    //创建UDP的socket
    SOCKET sClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sClient == INVALID_SOCKET)
    {
        cout << "创建套接字失败" << endl;
        return -1;
    }
    SOCKADDR_IN si;
    si.sin_family = AF_INET;
    si.sin_port = htons(PORT);
    si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    //开始绑定
    //bind(sClient, (sockaddr *)&si, sizeof(sockaddr));
    char szbuff[] = {
  
  "hello i am Client"};
    int dwSend = sendto(sClient, szbuff, sizeof(szbuff), 0, (sockaddr *)&si, sizeof(sockaddr));
    if (dwSend==0)
    {
        cout << "发送失败" << endl;
        return -1;
    }
    cout << "Client Send msg :" << szbuff << endl;
    char szRecvBuff[2048];
    memset(szRecvBuff,0, sizeof(szRecvBuff));
    SOCKADDR_IN addrServer = { 0 };
    int addrServerLen = sizeof(sockaddr);
    recvfrom(sClient, szRecvBuff, sizeof(sockaddr), 0, (sockaddr *)&addrServer, &addrServerLen);
    cout << "Client Recv msg from Serve:" << szRecvBuff << endl;
    closesocket(sClient);
    WSACleanup();
    return 0;
}

在代码中可以简单的看出UDP客户端与服务端交互的简单模式。而且传递数据时,把数据内容保存在数组中,传递给UDPServer,然后在recvfrom中会保存发送源的IP和端口信息,这样可以使得UDPServer同时可以向该地址信息发送数据。同样以字符数组保存数据。UDPServer程序运行以后,会一直运行,不断接受UDPClient发送过来的数据。

7.3 程序运行结果

7.3.1 UDPClient运行界面

7.3.2 UDPServer运行界面

UDPServer程序执行之后,一直运行,执行了两次UDPClinet程序,先后发送了两次数据。

8 引用

https://www.cnblogs.com/HPAHPA/p/7737531.html https://www.cnblogs.com/skyfsm/p/6287787.html https://www.cnblogs.com/chaguang/p/7118476.html

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/138597.html原文链接:https://javaforall.cn

0 人点赞