vs—socket—udp详细通信过程

socket和tcp/ip协议的关系

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

在这里插入图片描述

Socket基本概念

  • 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

  • 建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

  • Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

中文名 套接字
常用类型 流式socket和数据包式socket
相关模式 对等模式 、C/S模式
相关应用 c++、java、python
  • 应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
  • socket实际上提供进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。
  • 在网间网内部,每一个Socket用一个半相关模式:(协议,本地地址,本地端口)
  • 一个完整的socket有一个本地唯一的Socket号,由操作系统分配。
  • Socket是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的Socket系统调用。客户随机申请一个Socket(相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个Socket号;服务器拥有全局公认的Socket,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。 Socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器Socket半相关为全局所公认非常重要。读者不妨考虑一下,两个完全随机的用户进程之间如何建立通信?假如通信双方没有任何一方的Socket固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。

套接字连接的步骤

套接字之间的连接过程可以分为3个步骤:

  1. 服务器监听
    是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

  2. 客服端请求‘
    客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

  3. 连接确认
    当服务器套接字监听到或收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接受其他客户端套接字的连接请求。

常用函数

  • 创建

    1. 函数原型:
      int socket(int domain,int type,int protocol);

    2. 参数说明

      1. domain:协议域,又称协议族。
        1)常用的协议族有:AF_INET,AF_INET6,AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。
        2)协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
        3)用于确定哪个通讯模式。
      2. type:指定socket类型。
        1) 常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
        2) 流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
        3)数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
      3. protocol:指定协议。
        1)常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等。
      协议 对应的协议
      PROTO_TCP TCP传输协议
      ROTO_UDP UDP传输协议
      ROTO_STPC STCP传输协议
      PROTO_TICP TIPC
    3. 注意:

      1. type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
      2. WindowsSocket下protocol参数中不存在IPPROTO_STCP
    4. 返回值
      若调用成功,返回新创建的套接字的描述符,
      若失败,返回INVALID_SOCKET(Linux下返回-1)。
      套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。

  • 绑定

    1. 函数原型
      int bind(SOCKET socket,const struct socketAddr*,socklen_t address_len);
    2. 参数说明:
      1. socket
        一个套接字的描述符
      2. socketAddr*
        是一个sockaddr结构的指针,该结构包含了要结合的地址的端口号。
      3. address_len
        确定address缓冲区的长度。
    3. 返回值:
      函数执行成功,返回0,否则返回SOCKET_ERROR。
  • 接收连接请求

    1. 函数原型
      int accept(int fd,struct socketaddr* addr,socklen_t* len);
    2. 参数说明
    参数 参数说明
    fd 套接字描述符
    addr 返回连接者的地址
    len 接收返回地址的缓冲区长度
    1. 返回值
      若成功,返回客服端的文件描述符
      若失败,返回-1.
  • 接收

    1. 函数原型
      int recv(Socket socket,char FAR* buf,int len ,int flag);

      1. 参数说明:
        1. socket
          一个标识已经连接套接口的描述字。
        2. buf
          用于已经接收数据的缓冲区
        3. len
          缓冲区长度
        4. flag
          指定调用方式。
          取值:MSG_PEEK查看当前数据,数据将被复制到缓冲区中,但是并不从输入队列中删除。
      2. 返回值
        若无错误,返回读入的字节数。
        若连接已经中断,返回0.
        否则的话,返回SOCKET_ERROR错误
        应用程序可以根据WSAGetLastError()获取相应错误代码。
    2. 函数原型:
      ssize_t recvfrom(int sockfd,void buf,int len,unsigned int flags,struct socketaddr* from,socket_t* fromlen);

      1. 参数说明

        1. sockfd
          标识一个已经连接套接口的描述字。
        2. buf
          接收数据缓冲区
        3. len
          接收数据缓冲区的长度
        4. flags
          调用操作方式。是以下一个或者多个标志的组合体,可以通过or操作连在一起:
        MSG_DONTWAIT 操作不会阻塞。
        MSG_ERRQUEUE 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
        MSG_PEEK 指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
        MSG_TRUNC 返回封包的实际长度,即使它比所提供的缓冲区更长。只对Packet套接字有效。
        MSG_WAITALL 要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
        MSG_EOR 指示记录的结束,返回的数据完成一个记录。
        MSG_TRUNC 指明数据报尾部数据已被丢弃,因为它比所提供的缓存区需要更多的空间。
        MSG_CTRUNC 指明由于缓冲区空间不足,一些控制数据已被丢弃。
        MSG_OOB 指示接收到out-of-band数据。即需要优先处理的数据。
        MSG_ERRQUEUE 指示除了来自套接字错误队列的错误外,没有接受到其它数据。
        1. from(可选)
          指针,指向装有源地址的缓冲区。
        2. fromlen:(可选)
          指针,指向from缓冲区长度值
  • 发送

    1. 函数原型
      int sendto(SOCKET socket,const char FAR* buf,int size,int flags,const struct sockaddr FAR* to,int tolen);
    2. 参数说明
    参数 参数说明
    socket 套接字
    buf 待发送数据的缓冲区
    size 缓冲区长度
    flags 调用方式标志位,一般为0,改变Flags,将会改变sendto发送的形式
    addr(可选) 指向目的的套接字的地址
    tolen addr所指地址的长度
    1. 返回值
      若成功,返回发送的字节数
      若失败,返回SOCKET_ERROR

如何告知对方已发送完命令

其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。

  • 通过Socket关闭
    当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。

    但是这种方式有一些缺点

    1. 客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
    2. 如果客户端想再次发送消息,需要重现创建Socket连接
  • 通过socket关闭输出流的方式

    		这种方式调用的方法是:
    		      socket.shutdownOutput();
    		而不是(outputStream为发送消息到服务端打开的输出流):
    		     outputStream.close();
    
    1. 如果关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。
    2. 这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:
      不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接
      这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。
  • 通过约定的符号

    1. 这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。

      假如约定单端的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成:
      what is your name?
      end

      此时需要改造服务器的读取方式:

      Socket socket = server.accept();
      // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
      BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
      String line;
      StringBuilder sb = new StringBuilder();
      while ((line = read.readLine()) != null && "end".equals(line)) {
        //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
        sb.append(line);
      }
      

      可以看见,服务端不仅判断是否读到了流的末尾,还判断了是否读到了约定的末尾。

    2. 优缺点如下:

      优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)
      缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽

拆包和黏包

使用Socket通信的时候,或多或少都听过拆包和黏包,如果没听过而去贸然编程那么偶尔就会碰到一些莫名其妙的问题,所有有这方面的知识还是比较重要的,至少知道怎么发生,怎么防范。
  现在先简单说明下拆包和黏包的原因:

  • 拆包:当一次发送(Socket)的数据量过大,而底层(TCP/IP)不支持一次发送那么大的数据量,则会发生拆包现象。
  • 黏包:当在短时间内发送(Socket)很多数据量小的包时,底层(TCP/IP)会根据一定的算法(指Nagle)把一些包合作为一个包发送。

首先可以明确的是,大部分情况下我们是不希望发生拆包和黏包的(如果希望发生,什么都去做即可),那么怎么去避免呢,下面进行详解?

  • 黏包
      首先我们应该正确看待黏包,黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms),那么还是不建议关闭这种优化,如果因为黏包会造成业务上的错误,那么请改正你的服务端读取算法(协议),因为即便不发生黏包,在服务端缓存区也可能会合并起来一起提交给上层,推荐使用长度+类型+数据模式。
      如果不希望发生黏包,那么通过禁用TCP_NODELAY即可,Socket中也有相应的方法:
    void setTcpNoDelay(boolean on)
      通过设置为true即可防止在发送的时候黏包,但是当发送的速率大于读取的速率时,在服务端也会发生黏包,即因服务端读取过慢,导致它一次可能读取多个包。

  • 拆包
    最大报文段长度(MSS)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的 MSS。客户端会尽量满足服务端的要求且不能大于服务端的MSS值,当没有协商时,会使用值536字节。虽然看起来MSS值越大越好,但是考虑到一些其他情况,这个值还是不太好确定。
      如何应对拆包,那就是如何表明发送完一条消息了,对于已知数据长度的模式,可以构造相同大小的数组,循环读取,示例代码如下:

    int length=1024;//这个是读取的到数据长度,现假定1024
    byte[] data=new byte[1024];
    int readLength=0;
    while(readLength<length){
        int read = inputStream.read(data, readLength, length-readLength);
        readLength+=read;
    }
    
    

这样当循环结束后,就能读取到完整的一条数据,而不需要考虑拆包了。

还有很多关于socket编程问题,请参考 https://www.cnblogs.com/yiwangzhibujian/p/7107785.html

UDP协议

  • udp是一种面向无连接,不可靠的传输层协议。
  • upd的连接过程

    在这里插入图片描述

vs 实现udp通信

服务器端编程的步骤:

  1. 创建套接字(socket)
  2. 将套接字和IP地址、端口号绑定在一起(bind)
  3. 等待客户端发起数据通信(recvfrom/recvto)
  4. 关闭套接字

客户端编程的步骤:

  1. 创建套接字(socket)
  2. 向服务器发起通信(recvfrom/recvto)
  3. 关闭套接字

在这里插入图片描述

分析:

  • 在vs中一般使用Winsock2实现网络通信功能,所以需要引进头文件winsock2.h和库文件"ws2_32.lib"。

       1. WinSock2 是连接系统和用户使用的软件之间用于交流的一个接口,这个功能就是修复软件与系统正确的通讯的作用。
       
       2.  Winsock2 SPI(Service Provider Interface)服务提供者接口建立在Windows开放系统架构WOSA(Windows Open System Architecture)之上,是Winsock系统组件提供的面向系统底层的编程接口。
           Winsock系统组件向上面向用户应用程序提供一个标准的API接口;向下在Winsock组件和Winsock服务提供者(比如TCP/IP协议栈)之间提供一个标准的SPI接口。
           各种服务提供者是Windows支持的DLL,挂载在Winsock2 的Ws2_32.dll模块下。
           对用户应用程序使用的Winsock2 API中定义的许多内部函数来说,这些服务提供者都提供了它们的对应的运作方式(例如API函数WSAConnect有相应的SPI函数WSPConnect)。
           多数情况下,一个应用程序在调用Winsock2 API函数时,Ws2_32.dll会调用相应的Winsock2 SPI函数,利用特定的服务提供者执行所请求的服务。
           
    详细:  https://baike.baidu.com/item/winsock2/7907481?fr=aladdin
    
  • Windows下的库文件目录在哪里吧,以便以后使用(Windows10)

    C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86

  • # pragma comment(lib,“Ws2_32.lib”)
    表示链接Ws2_32.lib这个库。
    和在工程设置里写上链入Ws2_32.lib的效果一样,不过这种方法写的程序别人在使用你的代码的时候就不用再设置工程settings了。
    告诉连接器连接的时候要找ws2_32.lib,这样你就不用在linker的lib设置里指定这个lib了。
    ws2_32.lib是winsock2的库文件
    WinSock2就相当于连接系统和你使用的软件之间交流的一个接口,可能这个功能就是修复软件与系统正确的通讯的作用。

  • WASDATA
    WSADATA,一种数据结构。这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据。它包含Winsock.dll执行的数据。
    定义位置:Winsock.h

    结构原型: 
    typedef struct WSAData {
            WORD                    wVersion;   
            WORD                    wHighVersion;
    #ifdef _WIN64
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
    #else
            char                    szDescription[WSADESCRIPTION_LEN+1];
            char                    szSystemStatus[WSASYS_STATUS_LEN+1];
            unsigned short          iMaxSockets;
            unsigned short          iMaxUdpDg;
            char FAR *              lpVendorInfo;
    #endif
    } WSADATA,  FAR * LPWSADATA;
    
    各参数的含义: https://baike.baidu.com/item/WSADATA
    
  • MAKEWORD(a, b)
    makeword是将两个byte型合并成一个word型,一个在高8位(b),一个在低8位(a)
    返回值:一个无符号16位整形数。

    MAKEWORD(1,1)和MAKEWORD(2,2)的区别在于,前者只能一次接收一次,不能马上发送,而后者能。

    声明调用不同的Winsock版本。
    例如MAKEWORD(2,2)就是调用2.2版,MAKEWORD(1,1)就是调用1.1版。
    不同版本是有区别的,例如1.1版只支持TCP/IP协议,而2.0版可以支持多协议。
    2.0版有良好的向后兼容性,任何使用1.1版的源代码、二进制文件、应用程序都可以不加修改地在2.0规范下使用。
    此外winsock 2.0支持异步 1.1不支持异步.

    宏定义:#define MAKEWORD(a, b)  ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
    返回值:typedef unsigned short      WORD;
    参考: https://blog.csdn.net/happy_xiahuixiax/article/details/72637370
    
  • WSAStartup(sockVersion, &wsadata)
    WSAStartup,即WSA(Windows Sockets Asynchronous,Windows异步套接字)的启动命令。是Windows下的网络编程接口软件Winsock1 或 Winsock2 里面的一个命令。

    WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数。它允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。

    int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
    ⑴ wVersionRequested:一个WORD(双字节)型数值,在最高版本的Windows Sockets支持调用者使用,高阶字节指定小版本(修订本),低位字节指定主版本号。
    ⑵lpWSAData 指向WSADATA数据结构的指针,用来接收Windows Sockets 实现的细节。
    WindowsSockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
    
    参考:https://baike.baidu.com/item/WSAStartup/10237703?fr=aladdin
    
  • sockaddr结构

 truct sockaddr
 {
      unsigned short    sa_family;             /*addressfamily,AF_xxx*/
      char              sa_data[14];           /*14bytesofprotocoladdress*/
 } ;
sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族。
sa_data是14字节协议地址。

此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构(在WinSock2.h中定义):
struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序),在linux下,端口号的范围0~65535,同时0~1024范围的端口号已经被系统使用或保留。
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向sockaddr的结构体,并代替它。
也就是说,你可以使用sockaddr_in建立你所需要的信息,
然后用memset函数初始化就可以了memset((char*)&mysock,0,sizeof(mysock));//初始化

参考:https://baike.baidu.com/item/SOCKADDR_IN
  • c_str
    c_str是Borland封装的String类中的一个函数,它返回当前字符串的首字符地址。当需要打开一个由用户自己输入文件名的文件时,可以这样写:ifstream in(st.c_str())。
    在vc++2010中提示的错误原因:

    1. 	vc++2017应该这样用:
    	char c[20];
        string s="1234";
        strcpy(c,s.c_str());
    这样才不会出错,c_str()返回的是一个临时指针,不能对其进行操作
    c_str()返回的是一个分配给const char*的地址,其内容已设定为不可变更,如果再把此地址赋给一个可以变更内容的char*变量,就会产生冲突。但是如果放入函数调用,或者直接输出,因为这些函数和输出都是把字符串指针作为 const char*引用的,所以不会有问题。
    
    2. c_str()const char* 类型返回 string 内含的字符串
    如果一个函数要求char*参数,可以使用c_str()方法:
    string s = "Hello World!";
    printf("%s", s.c_str()); //输出 "Hello World!"
    
    3. c_str在打开文件时的用处:
    当需要打开一个由用户自己输入文件名的文件时,可以这样写:ifstream in(st.c_str());。其中st是string类型,存放的即为用户输入的文件名。
    
    参考:https://baike.baidu.com/item/c_str/2622670
    
  • memset
    在这里插入图片描述

    void *memset(void *s, int ch, size_t n);
    函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
    memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法 。
    memset()函数原型是:extern void *memset(void *buffer, int c, int count) 
                       buffer:为指针或是数组,
                       c:是赋给buffer的值,
                       count:是buffer的长度.
                       
    参考:https://baike.baidu.com/item/memset/4747579?fr=aladdin
    
  • recvfrom

    
    recvfrom(
        _In_ SOCKET s,
        _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, //接收数据的缓冲区     
        _In_ int len,                                                                 //缓冲区的大小
        _In_ int flags,                                                               //标志位,调用操作方式
        _Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from,     //sockaddr结构地址
        _Inout_opt_ int FAR * fromlen                                                 //sockaddr结构大小地址
        );
    
  • sendto

    WSAAPI
    sendto(
        _In_ SOCKET s,                                            //socket 
        _In_reads_bytes_(len) const char FAR * buf,               //发送数据的缓冲区   
        _In_ int len,                                             //缓冲区大小      
        _In_ int flags,                                           //标志位,调用操作方式
        _In_reads_bytes_(tolen) const struct sockaddr FAR * to,   //sockaddr结构地址
        _In_ int tolen                                            //sockaddr结构大小地址
        );
    

代码分析

新建项目:windows控制台应用程序 -->项目名称:server

// server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>
#include <WinSock2.h>
#include<WS2tcpip.h>
#include<string>
#pragma comment(lib,"ws2_32.lib")

using namespace std;

int main() {
    //设置版本号
	WORD sockVersion = MAKEWORD(2, 2);
    //定义一个WSADATA类型的结构体,存储被WSAStartup函数调用后返回的Windows Sockets数据
	WSADATA wsadata;
	//初始化套接字,启动构建,将“ws2_32.lib”加载到内存中
	if (WSAStartup(sockVersion, &wsadata)) {
		printf("WSAStartup failed \n");
		return 0;
	}
	//创建一个套接字,即创建一个内核对象
	SOCKET hServer = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (hServer == INVALID_SOCKET) {
		printf("socket failed \n");
		return 0;
	}
	//创建服务器端地址并绑定端口号的IP地址
	sockaddr_in addrServer;
	addrServer.sin_family = AF_INET;
	addrServer.sin_port = htons(8889);
	addrServer.sin_addr.S_un.S_addr = INADDR_ANY;

	// 初始化内核对象,传参给内核对象,此时数据可能都处于未就绪链表
	int nRet = bind(hServer, (sockaddr*)&addrServer, sizeof(addrServer));
	if (nRet == SOCKET_ERROR) {
		printf("socket bind failed\n");
		closesocket(hServer);
		WSACleanup();
		return 0;
	}
     //创建一个客服端地址
	sockaddr_in  addrClient;
	int nlen = sizeof(addrClient);
	//创建一个中间变量,用于存放用户输入的信息
	string str;
	//用于接受数据的缓冲区。
	char rcvdata[255];


	//可以循环接受数据
	while (true) {
		//接收数据:
			//初始化缓冲区,用于下一次数据的接收
			memset(rcvdata, 0, sizeof(rcvdata));
			//接受客户端的消息
			int ret = recvfrom(hServer, rcvdata, 255, 0, (SOCKADDR*)&addrClient, &nlen);
			if (ret > 0) {
				//缓冲区有数据,开始读取数据
				rcvdata[ret] = 0X00;
				//接收到结束标志,关闭服务器。
				if (rcvdata == "byebye") {
					//关闭服务器套接字
					closesocket(hServer);
					return 0;
				}
				printf(" ClientA:%s\n", rcvdata);
			}	
        //发送数据:
		cout << "Server:";
		//从键盘获取数据,存放在str中
		getline(cin, str);
		//建立发送数据缓冲区
		const int len = sizeof(str);
		char senddata[len];
		strcpy_s(senddata, str.c_str());
		//发送数据
        sendto(hServer, (char*)senddata, strlen(senddata), 0, (SOCKADDR*)&addrClient, nlen);
		str = "";
	}

	//关闭服务器套接字
	closesocket(hServer);
	WSACleanup();
	return 0;


}

新建项目:windows控制台应用程序 -->项目名称:client

// client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include"pch.h"
#include<iostream>
#include<WinSock2.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main() {

	//套接字信息结构
	WSADATA wsadata;
	//设置版本号
	WORD sockVersion = MAKEWORD(2, 2);
	//建立一个客户端套接字;
	SOCKET sClient;
	//启动构建,将“为ws2_32.lib”加载到内存中,做一些初始化工作
	if (WSAStartup(sockVersion, &wsadata) != 0) {
		//判断是否构建成功,若失败,则客户端打印一句提示话。
		printf("WSAStartup failed \n");
		return 0;
	}

	//创建客户端udp套接字
    sClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (SOCKET_ERROR == sClient) {
		printf("socket failed !\n");
		return 0;
	}

	//创建服务器端地址
	sockaddr_in serverAddr;
	//创建服务器端地址
	sockaddr_in clientAddr;
	//设置服务器端地址,端口号,协议族
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(8889);
	serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//获取服务器地址和客户端地址构造体的长度
	int slen = sizeof(serverAddr);
	int clen = sizeof(clientAddr);
	//设置接受数据缓冲区大小
	char buffer[2048] = { 0 };
	//用于记录发送函数和接受函数的返回值
	int iSend = 0;
	int iRcv = 0;
	string str;
	cout << "开始主动与服务器建立通信:" << endl;

	while (true) {
		//发送数据
			cout << "Client: ";
			getline(cin, str) ;
			//判断是否输入的是结束标记“byebye”
			if (str == "byebye") {
				printf("close connection \n");
				closesocket(sClient);
				return 0;
			}
			const int len = sizeof(str);
			char senddata[len];
			strcpy_s(senddata, str.c_str());

			iSend=sendto(sClient, (char*)senddata, strlen(senddata), 0, (SOCKADDR*)&serverAddr, slen);
			if (iSend== SOCKET_ERROR) {
				printf("sendto failed \n");
				closesocket(sClient);
				WSACleanup();
				return 0;
			}
			str = "";

		//接受客户端数据
		memset(buffer, 0, sizeof(buffer));
		iRcv= recvfrom(sClient, buffer, sizeof(buffer), 0, (SOCKADDR*)&clientAddr,&clen);
		if (iRcv == SOCKET_ERROR) {
			printf("recvFrom failed \n");
			closesocket(sClient);
			WSACleanup();
			return 0;
		}
		printf("Server: %s\n", buffer);
	}
	closesocket(sClient);
	WSACleanup();
	return 0;
}

将两个cpp文件都release后打开exe文件,进行正常通信。

c在这里插入图片描述

问题

  • 在VS2017中进行套接字编程时,
    sockaddr_in ClientAddr;
    ClientAddr.sin_addr.S_un.S_addr = inet_addr(“127.0.0.1”);

    在编译时会弹出
    error C4996: ‘inet_addr’: Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings错误提示。主要原因是inet_addr()函数已经过时,推荐使用inet_pton()或者InetPton()函数。

    问题解决
    可以采用三种方法解决error C4996错误:第一种是关闭项目的SDL检查;第二种是对_WINSOCK_DEPRECATED_NO_WARNINGS进行定义;第三种是使用推荐的新函数。如果想继续使用旧函数,可使用前两种方法。

    1. 关闭项目的SDL检查
      SDL叫做“安全开发声明周期”检查,是VS2012中新添加的功能。主要是为了能更好地监管该法着的代码安全。

    2. 定义_WINSOCK_DEPRECATED_NO_WARNINGS
      在项目的预编译头文件中添加对_WINSOCK_DEPRECATED_NO_WARNINGS的定义
      #define _WINSOCK_DEPRECATED_NO_WARNINGS 0
      将其定义为0、1、2…均可。

    3. 使用推荐的新函数
      inet_pton()函数或者InetPton()函数在Ws2tcpip.h中定义,在使用这些新函数之前需要包含该头文件。
      #include <Ws2tcpip.h>

猜你喜欢

转载自blog.csdn.net/qq_41498261/article/details/83097717