网络套接字(Udp与Tcp应用)

Posted 楠c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络套接字(Udp与Tcp应用)相关的知识,希望对你有一定的参考价值。

在网络通信中,凡是我们所写的代码,采用的接口都是系统调用接口,编写的程序都叫用户层程序,我们接下来的工作就是在用户层自定义协议。在网络模型中就是应用层,那么就是要使用传输层的接口(但是有原始套接字可以绕过传输层)

1. 认识套接字

1.1 IP

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
有了IP地址能够把消息发送到对方的机器上,但是跨网络传输还需要有一个其他的标识来区分出, 这个数据要给对方的哪个程序进行解析。
即IP在公网当中全网标识一台主机,发送的时候,不仅需要目的IP,通信也要自己的源IP也发过去,因为对方主机还要对你做出“回应”。

在你打开网页,访问百度的时候,实际上硬件只是一个载体,实际上通信的是你笔记本上的软件(浏览器进程),和对方服务器上的软件(服务器进程)。更准确的一点说,实际上是运行起来的进程进行通信,所以套接字的本质就是跨网络的进程间通信。

1.2 端口号

一个笔记本上,有很多进程,所有的进程并发的进行运行。所以通信的时候还需要一个东西来标识某个进程,标定进程的方式叫端口号。

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用
    传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;就像你到了那台主机之还要根据目的端口号找到对应进程

1.3 套接字

IP标记某个全网唯一主机。
端口号标识主机内为一进程。那么IP+端口号就实现了标识全网内的唯一进程。而这个IP+端口号就是套接字。
在这里插入图片描述

服务器几乎永远不会关机,只会不断更新。虽然这里只简单的花了两个进程。但是这两台主机中充斥着大量的进程。公网IP保证了主机的唯一性,端口号保证了进程的唯一性。进程间通信,不同的进程看到了同一套资源,而跨网络进程通信,不同主机的进程就看到的是网络这个资源。

网络之中充斥着大量的套接字就要被管理起来
在这里插入图片描述
这里面有一个熟悉的file而file中存在一个
在这里插入图片描述
又指向这个socket指针
在这里插入图片描述
ops指针中存在着各种函数指针
在这里插入图片描述

1.4 端口号和进程id

pid表示唯一一个进程,并不是所有的进程都需要端口号,但是所有的进程在系统层面上都有一个pid。只有你这个进程是网络进程时才需要端口号。
一个进程可以绑定多个端口号
一个端口号只能用于一个进程

最开始收到数据的一定是计算机当中的网卡,然后自底向上交付。

1.5 认识TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输(要保证处理数据,丢包,等等,比较复杂)
  • 面向字节流
    管道也是面向字节流的。

1.6 认识UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.7 网络字节序

低字节位在低地址处,叫做小端。
高字节位在低地址处,叫做大端。
假如你发数据,对方服务器可不知道你的数据你发的数据是大端还是小端,假如你的笔记本是小端,对方服务器是大端,那么服务器就会数据理解错误,这种情况肯定是存在,而且要被解决的。
网络规定,网络上跑的数据默认大端。假如你是小端机操作系统就会默认转为大端,收方默认接受的就是大端数据。

假设要发送0x1234abcd

在这里插入图片描述

  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
    大端发送很方便,由于首先收到的就是高权值数据,所以就可以边计算便接收

1.8 库函数

大端就不做任何转换,小端机调用这些函数,将数据转换成大端序列。
在这里插入图片描述
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址(点分十进制,四个.,一个点隔开一个字节,每个范围为0-255)转换后准备发送

1.9 地址转换函数

在这里插入图片描述
这个inet_ntoa函数返回一个char*,实际把它存储在静态存储区,也就是说,作为一个静态局部变量,虽然他的作用域依旧在函数内,但是生命周期却变成了整个文件。也就是说多线程,会有线程安全问题。新的一次会把老的一次覆盖掉。虽然在当前环境测试没有出现问题,可能是新版本添加了互斥锁。但是不推荐使用这个函数,可以用inet_ntop来代替。

1.10 socket 常见API

  1. 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

domain代表域,即使用TCP或者UDP
在这里插入图片描述

套接字类别,流式套接字,数据报套接字,原始套接字
在这里插入图片描述

协议,操作系统使用默认行为

在这里插入图片描述

最重要的是返回值
在这里插入图片描述
可以这么理解,网卡也是一种文件,通信之前需要将文件打开,这里的socket函数等价于open,返回值等价一个文件描述符。

  1. 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);

关联IP,端口号。服务器一般不发消息,永远是被动的,即绑定的,IP,端口号是客户端自己的。在系统方面表明,将IP信息与网络信息关联起来。

  • 描述符
  • 将IP地址,端口号,填进去强转之后的结构体,然后绑定。
  • 结构体长度
  1. 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
  • 文件描述符
  • 由于是面向连接的,所以需要一个等待队列,backlog代表底层连接的长度,一般不要设置太大
  1. 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
  • 参数socket用于接收到链接请求(唯一一个),返回值socket用于通信(多个)
  1. 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

1.11 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同,操作系统实现了一种套接字接口,来解决不同套接字的编写,调用。
他就是sockaddr结构,就是用于将我们的IP,端口号等填入结构体,发给别人
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
在这里插入图片描述

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址

  • 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结构体指针做为参数。那么强转后,在函数内怎么区分呢?由于每个字段都是16位,把他们提取出来,用if判断类型。就好了

在这里插入图片描述

2. Udp服务器

2.1 收发接口

服务器被动的收发数据,所需要接口。

收数据

在这里插入图片描述

  • 文件描述符
  • buf缓冲区
  • 期望读的长度
  • flags,读取条件是否成立
  • 输出型参数,传入方信息
  • 既做输入又做输出,输入代表结构体大小,输出代表读取结构体大小

发数据
在这里插入图片描述

  • 文件描述符
  • 发送缓冲区
  • 期望发的长度
  • 读取条件是否成立
  • 刚才收数据收到了对方的信息
  • 输入代表结构体大小,输出代表读取大小

2.2 udp服务器实现

实际上这里的ip,可以不用输入,直接在填充的时候选择INADDR_ANY,这样在客户端输入任意IP,输入端口号,都可以访问服务器。

#pragma once 

#include<iostream>
#include<stdlib.h>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;

class udpServer
{
  private:
    string ip;
    int port;

    int sock;
  public:
    udpServer(string _ip="127.0.0.1",int _port=8080)
       :ip(_ip)
       ,port(_port)
    {}
    void initServer()
    {
      //创建socket描述符,默认为3
      sock=socket(AF_INET,SOCK_DGRAM,0);
      cout<<"sock:"<<sock<<endl;
      //填充信息到sockaddr _in中
      struct sockaddr_in local;
      local.sin_family=AF_INET;
      //转成大端
      local.sin_port=htons(port);
      //sockaddr中有一个sin_addr结构体,结构体中的saddr为ip
      //将ip转为char*
      local.sin_addr.s_addr=inet_addr(ip.c_str());


      //绑定端口号
      //可以让不同类型套接字,使用同一套接口,所以要强转
      //为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。
      if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
      {
        cerr<<"bind error!\\n"<<endl;
        exit(1);
      }
    }  
      void strat()
      {
        char msg[64]={0};
        for(;;)
        {
          //远端的信息
          
          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)
          {
            msg[s]='\\0';
            cout<<"client##"<<msg<<endl;
            string echo_string=msg;
            echo_string+="[注:服务器回显]";
            sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);

          }

        }
      }
      

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

main函数只需要简单的启动即可

#include"udpServer.hpp"

int main()
{
  udpServer *up=new udpServer;
  up->initServer();
  up->strat();
  delete up;
  return 0;
}

由于此时还没有客户端,那么怎么看到他运行起来了呢
用netstat -nlup命令,其中u代表udp,假如是 -bltp就是tcp。
在这里插入图片描述
服务端已启动。

3. udp客户端实现

#pragma once 

#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;

class udpClient
{
  private:
    string ip;
    int port;

    int sock;
  public:
    //连接服务器,服务器ip,port。
    udpClient(string _ip="127.0.0.1",int _port=8080)
       :ip(_ip)
       ,port(_port)
    {}
    void initClient()
    {
      //创建socket描述符,默认为3
      sock=socket(AF_INET,SOCK_DGRAM,0);
      cout<<"sock:"<<sock<<endl;
      
      //客户端不需要绑定
      //填充信息到sockaddr _in中
      //struct sockaddr_in local;
      //local.sin_family=AF_INET;
      //转成大端
      //local.sin_port=htons(port);
      //sockaddr中有一个sin_addr结构体,结构体中的saddr为ip
      //将ip转为char*
      //local.sin_addr.s_addr=inet_addr(ip.c_str());


      //绑定端口号
      //可以让不同类型套接字,使用同一套接口,所以要强转
      //为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。
     // if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    // {
       // cerr<<"bind error!\\n"<<endl;
       // _exit(1);
      //}
    }  
      void strat()
      {

          string msg;
          struct sockaddr_in peer;
          peer.sin_family=AF_INET;
          peer.sin_port=htons(port);
          //点分十进制转成4字节,主机序列转成网络序列
          peer.sin_addr.s_addr=inet_addr(ip.c_str());
        for(;;)
        {

          cout<<"请输入"<<endl;
          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,nullptr,nullptr);
          
          if(s>0)
          {
            echo[s]='\\0';
            cout<<"server###"<<echo<<endl;
          }

        }
      }
      

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

main中初始化,然后启动

#include"udpClient.hpp"

int main()
{
  udpClient uc;
  uc.initClient();
  uc.strat();
  return 0;
}

这里提到客户端不需要自己绑定,但是为什么服务器就需要绑定呢?
服务器:

  1. 一般服务器端口是总所周知的,ip和port不需要也不能轻易的更改。比如:http对应的端口号是80 https:443 ssh:22

  2. 服务器面对的客户很多,服务器一旦改了,客户端立马找不到,就无法访问服务器了。

客户端:

  1. 客户有很多客户端,如果绑定,就需要规定什么软件用什么端口,端口是标识进程的,一个端口只能对应一个进程,如果多个进程使用同一个端口,就会导致绑定是失败。并且这种让不同的公司进行沟通进行约定,是很不现实的。如果进行了bind会发生端口冲突,导致客户端无法启动

  2. 客户端需要唯一性,但不需要明确告诉你是哪个端口,因为也没有人去连接你,但是必需要IP和port。客户端使用udp服务器进行数据的交互之时,系统会自动进行Ip和端口号的绑定。

3.1 实验现象

在这里插入图片描述
客户端发数据,服务端收到,并回显到客户端。

而我们也可以,在main函数中传入参数,在命令行中带入ip与port,输入失败的时候,提示帮助手册。

在这里插入图片描述
客户端传入服务器的ip,port
在这里插入图片描述

3.2 本地环回

127.0.0.1,通常用来进行网络通信代码的本地测试,一般把网络层全部自顶向下,自底向上,跑一遍。进行测试。

4. 单进程Tcp

4.1 服务器

#ifndef __TCP__SERVER_H_
#define __TCP__SERVER_H_


#include<iostream>
#include<string>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>

using namespace std;

class tcpServer
{
  private:
    int port;
    int l_sock;
  public:
    tcpServer(int _port)
     :port(_port)
      ,l_sock(-1)
    {}
    void initServer()
    {
      l_sock=socket(AF_INET,SOCK_STREAM,0);
      if(l_sock<0)
      {
        cerr<<"socket error"<<endl;
        exit(2);
      }
      struct sockaddr_in local;
      local.sin_family=AF_INET;
      local.sin_port=htons(port);
      local.sin_addr.s_addr=INADDR_ANY;

      if(bind(l_sock,(struct sockaddr*)&local,sizeof(local))<0)
      {
           cerr<<"bind error"<<endl;
           exit(3);
      }
      if(listen(l_sock,5)<0)
      {
          cerr<<"bind error"<<endl;
          exit(4);
      }
    }
    void service(int sock)
    {
      while(true)
      {
        //读取甚至可以用read
        //udp用recvfrom,tcp用recv
        //写可以用write
        //udp用sendto,tcp用send
        char buffer[24]={0};
        size_t s=recv(sock,buffer,sizeof(buffer)-1,0);
        if(s>0)
        {
          buffer[s]={0};
          cout<<"client#: "<<buffer<<endl;

          send(sock,buffer,strlen(buffer),0);
        }
        //不写这句他就会阻塞在send或recv,写上,当s==0时就退出
        else if(s==0)
        {
              cout<<"client quit"<<endl;
              close(sock);
              break;
        }
        else{
         cout<<"recv client data error"<<endl;
         break;
        }
      }
      close(sock);
      
    }
    void start()
    {
      sockaddr_in endpoint;
      while(true)
      {
        //重新获取一个socket,加上原来的此时共有两个
        socklen_t len=sizeof(endpoint);
        int sock=accept(l_sock,(struct sockaddr*)&endpoint,&len);
        if(sock<0)
        {
          cerr<<"accept error"<<endl;
          continue;
        }
        cout<<"get a new link"<<endl;
        //当客户端退出,service也应该退出
         service(sock);
      }
    }
    ~tcpServer()
    {
      close(sock);
    }

};



#endif 

全0表示任意IP都可以。
在这里插入图片描述
虽然没有客户端,但是远程登录工具可以登录服务器
在这里插入图片描述
在这里插入图片描述
也可以进行通信
在这里插入图片描述

4.2 客户端

#include<iostream>
#include<string>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>


using namespace std;


class tcpClient{
  private:
    int svr_port;
    string svr_ip;
    int sock;
  public:
    tcpClient(string _ip="127.0.0.1",int port=8080)
    :svr_port(port)
     ,svr_ip(_ip)
    {}
    void initClient()
    {
     sock=socket(AF_INET,SOCK_STREAM,0);
     if(sock<0)
     {
       cerr<<"sock error"<<endl;
       exit(2)以上是关于网络套接字(Udp与Tcp应用)的主要内容,如果未能解决你的问题,请参考以下文章

TCP与UDP的区别

网络LinuxLinux网络编程-TCP,UDP套接字编程及代码示范

网络LinuxLinux网络编程-TCP,UDP套接字编程及代码示范

TCP和UDP套接字编程 (java实现)

Java-Web学习-Java基础-网络编程(TCP和UDP)附TCP实现通信

Java网络编程之UDP和TCP套接字