02 socket套接字编程---udp服务端和客户端

文章目录

  • 一、理解网络通信
  • 二、初步认识TCP、UDP协议
    • 2.1. TCP协议
    • 2.2. UDP协议
  • 三、网络字节序
    • 3.1. 主机字节序转换为网络字节序(host to network)
    • 3.2. 网络字节序转换为主机字节序( to network)
  • 四、socket编程接口
    • 4.1. sockaddr结构
      • 4.1.1. sockaddr_in结构体参数填充
    • 4.2. 创建socket文件描述符
    • 4.3. 绑定端口号
      • 4.3.1. 为什么服务器端要绑定而客户端不需要绑定
    • 4.4. UDP收发接口
      • 4.4.1. 接收函数
      • 4.4.2. 发送函数
  • 五、跨网络UDP服务端和客户端
    • 5.1. 服务端
    • 5.2. 客户端
    • 5.3. 实验现象


一、理解网络通信

我们进行上网都需要打开软件(上网入口),比如游览器,当打开软件之时,将硬盘上的文件加载到内存之中,在客户端启动了客户端进程。而在服务器上也有对应的服务器进程。然后客户端进程通过网络寻找对应的服务器进程,进行数据的交互。其本质就是进程之间的通信
在这之中,ip标识全公网内唯一一台主机、端口号port表示该主机中唯一的网络进程、,所以ip+port就是网络之中唯一的进程,即形成了进程之间的通信。

其中ip+port就是套接字,也就是说,套接字本质就是进程间通信

而通信需要进程双方看到同一份公共资源,这个资源就是网络:
02 socket套接字编程---udp服务端和客户端_第1张图片
02 socket套接字编程---udp服务端和客户端_第2张图片


二、初步认识TCP、UDP协议

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号,就是在描述 “数据是谁发的, 要发给谁”。

2.1. TCP协议

特点:面向连接,可靠传输,面向字节流

面向连接:TCP通信双方在发送数据之前,需先建立起连接,才能够发送数据。
可靠传输:TCP保证传输的数据是可靠有序的到达对端。
面向字节流:

  1. 对于传输的数据之间没有明显的数据边界(比如第一次发送123,第二次发送456,接收方是不能区分第一次发送的是什么)。
  2. 对于接收方而言,在可以接收数据的情况下,可以接收任意字节的数据(比如将12345,分好几次进行接收)。

02 socket套接字编程---udp服务端和客户端_第3张图片

TCP是提供可靠的传输层协议,因此需要处理的事情就会更多,比如数据会不会丢失,丢失了怎么办等等,因此TCP协议就会更加的复杂,复杂的东西效率也会更低

2.2. UDP协议

特点:无连接,不可靠,面向数据报

无连接:UDP通信双方在发送数据之前,是不需要进行沟通的,客户端只需要知道服务端的ip和端口,就可以直接发送数据了。
不可靠:不能保证数据能到达目的地,并且不保证数据是按序到达(比如先发1在发2,结果是2先到的)。
面向数据报:UDP对于传输层和应用层数据交递的时候,都是整条数据交付。

02 socket套接字编程---udp服务端和客户端_第4张图片

DUP只负责数据传输,不保证数据是安全达到的,因此UDP协议比较简单,效率也更高


三、网络字节序

有些系统的本机字节序是小端字节序, 有些则是大端字节序, 为了保证传送顺序的一致性, 所以网际协议使用大端字节序来传送数据

这意味着如果是小端机器在传输数据的时候,需要将数据转化为大端字节序进行传输,对端机器默认传输过来的数据是大端字节序。
02 socket套接字编程---udp服务端和客户端_第5张图片

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  3. 因此,网络数据流的地址应这样规定:先发出的数据是低地址的数据(在大端中对应高字节),后发出的数据是高地址(在大端中对应低字节);
  4. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

3.1. 主机字节序转换为网络字节序(host to network)

2个字节  uint16_t htons(uint16_t hostshort)
4个字节  uint32_t htonl(uint32_t hostlong)

3.2. 网络字节序转换为主机字节序( to network)

2个字节  uint16_t ntohs(uint16_t netshort);
4个字节  uint32_t ntohl(uint32_t netlong);

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

02 socket套接字编程---udp服务端和客户端_第6张图片


四、socket编程接口

4.1. sockaddr结构

操作系统设计sockaddr结构是为了实现一套接口就能完成不同套接字之间的通信(在接口传参时都传入通用结构)。通信前、要将自己的ip地址和端口号发送给对方,因此需要定义一个结构体来保存自己的ip地址和端口号。

02 socket套接字编程---udp服务端和客户端_第7张图片

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr * 类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
    02 socket套接字编程---udp服务端和客户端_第8张图片

4.1.1. sockaddr_in结构体参数填充

02 socket套接字编程---udp服务端和客户端_第9张图片

02 socket套接字编程---udp服务端和客户端_第10张图片

  • 当我们使用ipv4版本的ip协议,写网络协议,绑定地址信息的时候,需要填充struct sockaddr_in结构体来保存服务器的ip和侦听的端口。
  • 在使用bind函数的时候,需要将struct sockaddr_in结构体强转为struct sockaddr结构体。
  • 对于struct sockaddr_in和struct sockaddr结构体而言,前面两个字节都表示地址域,当我们强转strut sockaddr_ in结构体传入内核之后,内核是通过前面两个字节(地址域这两个字节)来确定后续的空间如何进行读取(内核读取了前面两个字节的地址域,然后获悉了传入的AFINEF协议,因此后续获得填充方式:先填充2个字节,再填充4个字节)。
  • struct sockaddr结构体是地址信息通用结构体,在绑定时都需要把其他两种结构体强转为该结构体。

在填入参数时,除了需要将主机字节序转化为网络字节序以外,还需要把ip地址从字符串类型转化为四字节的网络字节序(大端)的ip地址。
02 socket套接字编程---udp服务端和客户端_第11张图片

为什么在数据结构 struct sockaddr_in 中, sin_addr 和 sin_port 需要转换为网络字节顺序,而sin_family 需不需要呢?

sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要是网络字节顺序。但是 sin_family 域只是被内核 (kernel) 使用来决定在数据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时,sin_family 没有发送到网络上,它们可以是本机字节顺序。

综上,可以如下填充:
02 socket套接字编程---udp服务端和客户端_第12张图片

4.2. 创建socket文件描述符

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

domain:地址域
该参数指定网络层使用什么协议
AF_INET:使用ipv4版本的ip协议
AF_INET6:使用ipv6版本的ip协议
AF_UNIX :本地域套接字(适用与一台机器两个进程,进行进程间通信)

type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:用户数据报套接字

protocol:使用的协议协议
0:采用套接字类型对应的默认协议
SOCK_DGRAM :默认的协议就是UDP
SOCK_STREAM:默认的协议就是TCP


返回值:
返回一个套接字句柄,本质上是一个文件描述符
创建失败返回-1,成功返回值大于等于0

因为linux之中一切皆文件,要实现网络通信就需要打开网卡设备,所以创建struct file文件对象,帮我们指向网络信息。
因此socket的返回值是一个文件描述符。

创建网络通信相关的数据结构,这些结构在Linux之中统一被当做文件看待,进程打开文件的方式就是通过文件描述符。类似于open函数。
02 socket套接字编程---udp服务端和客户端_第13张图片

4.3. 绑定端口号

创建完套接字之后,对应的文件之中只有文件信息,而我们创建的是网络文件,因此需要填入ip、port,将文件信息和网络信息关联起来。
ip和端口号标识网络之中的唯一进程,可以让客户端找到自己。
有了文件信息和网络信息,但是他们之间没有关系,绑定就是让他们之间产生关系。

#include 
int bind(int socket, const struct sockaddr *address,
			socklen_t address_len);

sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口号进行绑定

address:给套接字绑定的ip地址和端口号,
	address的类型要被强转为通用结构类型struct sockaddr*
	
address_len:address的长度

返回值:绑定成功返回0,失败返回-1

02 socket套接字编程---udp服务端和客户端_第14张图片

4.3.1. 为什么服务器端要绑定而客户端不需要绑定

  • IP地址和端口号是用来标识具体某一台主机上的具体一个进程的。也就是说,端口号可以用来标识主机上的某一个进程。一般服务器端是固定的,ip和port不需要也不能轻易的更改。比如:http对应的端口号是80 https:443 ssh:22。
  • 因此,操作系统需要对端口号进行管理,并且计算机中的端口号是有限的。
  • 如果不进行绑定,操作系统会随机生成一个端口号给服务器。如果操作系统给服务器分配这个端口号的同时,有其他程序也准备使用这个端口号或者说端口号已经被使用,则可能会导致服务器一直启动不起来。
  • 其次,服务器运行起来就不会在停止了,我们将服务器端的端口号绑定有助于有规划的对主机中的端口号进行使用。并且,如果不绑定,一旦服务器重启,端口号可能会发生改变,那之前连接的客户端都要重新连接。
  • 客户端需要主动向服务器端发送请求,因此客户端就需要知道服务器端的IP地址和端口号,如果不绑定让系统随机生成,客户端将无法知道服务器端的端口号,即使知道也需要每次都去获取。

===

  • 对于客户端来说,服务器端并不需要主动给客户端发送数据,客户端是主动的而服务器端是被动的。客户端给服务器端发送数据时,会将自己的IP地址和端口号一起发送过去,服务器端可以方便的找到客户端。
  • 同时,客户端并不是一直运行的,只需要每次系统随机分配即可(操作系统最清楚端口号情况),并且自己绑定还会出现绑定失败的情况。
  • 客户有很多客户端,如果绑定,就需要让不同的公司商量一下,什么软件用什么端口,端口是标识进程的,一个端口只能对应一个进程,如果多个进程使用同一个端口,就会导致绑定是失败。并且这种让不同的公司进行沟通进行约定,是很不现实的。
  • 因此,服务器端需要绑定而客户端不需要绑定。

4.4. UDP收发接口

由于UDP并不是面向连接的,所以只需要服务器启动,就可以直接收发消息。

4.4.1. 接收函数

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

sockfd:socket返回的文件描述符
buf:读数据缓冲区
len:期望读取的数据长度
flags:读数据是IO、不一定有数据让你读,如果读取条件不成立,就挂起等待(默认为0,阻塞等待)
src_addr:用来获取发送方的sockaddr,也就是ip地址和端口号数据,如果不关心可以设置为空
addrlen:是一个整数,是实际读到结构体src_addr的大小,这个参数必须要进行初始化

返回值:
实际收到多少个字节的数据,如果为-1则接收错误
size_t无符号整形
ssize_t有符号整形

4.4.2. 发送函数

#include 
#include 
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,阻塞等待)
src_addr:发送方的sockaddr,也就是ip地址和端口号数据
addrlen:dest_addr结构体的大小

02 socket套接字编程---udp服务端和客户端_第15张图片


五、跨网络UDP服务端和客户端

5.1. 服务端

udpServer.hpp:

#pragma once 
#include 
#include
#include
#include 
#include 
#include 
#include
#include 
#include
class udpServer{
private:
  //std::string ip;
  int port;//端口号
  int sock;//套接字
public:
  //127.0.0.1本地环回,8080默认端口号
  //通常用来进行网络代码的本地测试
  udpServer(int _port=8080)
    :port(_port)
  {}
  void initServer()
  {
    //socket的返回值为文件描述符,类似于open函数
     sock=socket(AF_INET,SOCK_DGRAM,0);
     std::cout<<"sock:"<<sock<<std::endl;
     struct sockaddr_in local;
    
     local.sin_family=AF_INET;
     local.sin_port=htons(port);
     //转换ip
     //local.sin_addr.s_addr=inet_addr(ip.c_str());

     //INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP
     local.sin_addr.s_addr=INADDR_ANY;

     //绑定套接字,ip地址和端口号
     if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
       std::cerr<<"bind error!\n"<<std::endl;
       exit(1);
     }
     
  }
  void start()
  {
    char msg[64];
    //死循环,服务器不退出
    for(;;)
    {
        msg[0]='\0';
        struct sockaddr_in end_point;
        socklen_t len=sizeof(end_point);
        ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
        if(s>0)
        {
          //拿到发送端的ip地址,将整形四字节转成点分十进制的字符串
          std::string cli=inet_ntoa(end_point.sin_addr);
          cli+=":";
          //拿到发送端的端口号
          //要将网络字节序转为主机字节序,并且再转成字符串
          cli+=std::to_string(ntohs(end_point.sin_port));

          msg[s]='\0';
          std::cout<<cli<<"#"<<msg<<std::endl;
          //再给客户端发消息
          std:: string echo_string=msg;
          echo_string+="[server echo!]";
          sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
        }
    }
  }
  ~udpServer()
  {
    close(sock);
  }

};

udpServer.cpp:

#include "udpServer.hpp"
void Usage(std::string proc)
{
  std::cout<<"Usage:"<<proc<<"local_port"<<std::endl;
}
int main(int argc,char* argv[])
{
  if(argc!=2)
  {
    Usage(argv[0]);
    exit(1);
  }
  //绑定端口号
  udpServer *up=new udpServer(atoi(argv[1]));
  up->initServer();//将服务器初始化
  up->start();
  delete up;
  return 0;
}

5.2. 客户端

udpClient.hpp:

#pragma once 
#include 
#include
#include
#include 
#include 
#include 
#include
#include 
class udpClient{
private:
  std::string ip;
  int port;//端口号
  int sock;
public:
  //连服务器的ip和端口号
  udpClient(std::string _ip="127.0.0.1",int _port=8080)
    :ip(_ip)
     ,port(_port)
  {}
  void initClient()
  {
    //创建socket,客户端不需要绑定
    sock=socket(AF_INET,SOCK_DGRAM,0);
    std::cout<<sock<<std::endl;
  }
  void start()
  {
    std::string msg;
    //发送给谁
    struct sockaddr_in peer;
    peer.sin_family=AF_INET;
    peer.sin_port=htons(port);
    peer.sin_addr.s_addr=inet_addr(ip.c_str());
    for(;;)
    {
      std::cout<<"Please Enter#";
      std::cin>>msg;

      if(msg=="quit")
      {
        break;
      }
      sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
      char echo[128];
      //接收服务端发回来的消息
      ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,NULL,NULL);
      if(s>0)
      {
        echo[s]=0;
        std::cout<<"server#"<<echo<<std::endl;
      }

    }

  }
  ~udpClient()
  {
    close(sock);
  }

};

udpClient.cpp:

#include "udpClient.hpp"
void Usage(std::string proc)
{
  std::cout<<"Usage:"<<proc<<"server_ip server_port"<<std::endl;
}
//通过参数列表获取输入的ip和端口号,该ip和端口号就是服务器的ip和端口号
int main(int argc,char* argv[])
{
  if(argc!=3)
  {
    Usage(argv[0]);
    exit(1);
  }
  //绑定ip地址和端口号,端口号是一个整数,要进行强转
  udpClient uc(argv[1],atoi(argv[2]));
  uc.initClient();
  uc.start();
  return 0;
}

5.3. 实验现象

02 socket套接字编程---udp服务端和客户端_第16张图片

你可能感兴趣的:(计算机网络,网络协议)