网络网络编程

Posted 山舟

tags:

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

文章目录


一、预备知识

1.源IP地址、目的IP地址

源IP地址指的就是发送数据包的那个主机的IP地址。

目的IP地址就是想要发送到的那个主机的IP地址。

IP地址可以标识全网内唯一的一台主机。


2.端口号(port)

端口号(port)是传输层协议的内容。

  • 端口号是一个2字节、16比特位的整数。
  • 端口号用来标识一个进程,告诉操作系统当前数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够唯一标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。

任何的网络服务或网络客户端,如果要进行正常的数据通信,必须要使用端口号来唯一标识自己。一个进程可以与一个端口号绑定,再加上主机IP地址该端口号就在网络层面上唯一标识一台主机上的唯一一个进程。

这种IP+port标识的方案叫做socket通信


PID vs PORT

一台机器上会存在大量的进程,为了区分所有的进程,设计了PID来加以区分(系统的概念);但是只有部分进程需要进行网络数据请求,所以用port来标识这些需要进行网络数据请求的进程(网络的概念)。

这类似于身份证号可以唯一标识每一个人,但是在学校里又用学号来唯一标识每一个人。身份证号可以看做PID,学号可以看做port,它们之间并不冲突,都是在各自场景下最合适的管理方案。


3.TCP、UDP

这里仅很简略地介绍两个协议,之后会详细讲解。

(1)TCP

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

(2)UDP

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

4.网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
所以不管主机是大端机还是小端机,都会按照TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。


函数

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

这些函数名很容易理解,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序后,从主机向网络发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。


5.IP地址的表示

一般IP地址会表示为点分十进制字符串的风格,即"192.168.230.125"这样,且每一部分数据的范围是0~255。

但IPV4通常是32比特位的数据,而注意到上面每一部分数据的范围,所以将每个数字都用8比特位来表示,整体刚好用一个32比特位的无符号整数来表示。


二、socket编程函数

下面的这些函数在这里先从理论上解释一下,后面代码中基本都会实际用到,也会附有相应的注释,所以最好先向下看代码,结合代码来看函数。

1.socket

该函数打开一个网络通讯端口,如果成功,就像open()一样返回一个文件描述符。

(1)domain

这个选项是域,也就是各种协议,有如下选项。对于IPv4,该参数指定为AF_INET。

(2)type

即服务类型,有如下选项,其中SOCK_STREAM对应TCP,SOCK_DGRAM对应UDP,下面的代码主要用这两种来编写。

(3)protocol

即协议类别,一般设置为0即可,因为该函数会通过前两个参数自动推导出第三个参数的协议类别。

(4)返回值


注意它的返回值是一个文件描述符。成功返回对应的文件描述符,失败返回-1。


2.bind

服务器程序所的网络地址(IP地址)和端口号(port)通常是固定不变的,而客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号。

(1)sockfd

这个参数传入上面socket的返回值即可。

(2)addr

这是一个sockaddr类型的结构体。

socket相关接口是一层抽象的网络编程接口,适用于各种底层网络协议,包括IPv4、IPv6、以及后面要讲的UNIX Domain Socket等等。然而, 各种网络协议的地址格式并不相同。

如下面三种地址格式。


这里查看先查找sockaddr_in的定义所在的位置,然后查看其内容:


虽然接口的参数是sockaddr,但是下面基于IPv4编程时,使用的数据结构是sockaddr_in。

(3)addrlen

传入第二个参数addr的大小即可。


3.recvfrom

从sockfd中读数据,读入的内容放入buf内,期望读到的个数是len,实际读到的个数作为返回值返回,flags是读取的方式,可设为0;剩下的src_addr和addrlen是表示发送内容的对端addr的相关信息(类似于上面函数sockaddr_in的内容,但这里是sockaddr,还略有区别)。


4.不同类型IP地址之间的转换函数


5.sendto


从参数来看和recvfrom基本相同,所以用法也很像。

向sockfd中写如buf内的数据,期望写入的个数是len,实际写入的个数作为返回值返回,flags是读取的方式,可设为0;剩下的src_addr和addrlen是表示发送内容的对端addr的相关信息(类似于上面函数sockaddr_in的内容,但这里是sockaddr,还略有区别)。


三、简单的UDP网络程序

下面的四段代码重点在两段实现类的hpp文件中,新用到的函数更是重中之重;两段主函数逻辑比较简单,主要用来启动服务器和客户端。

1.服务器实现

主要就是实现一个UdpServer类,重要的内容(主要是函数调用)都附在注释中。

//udp_server.hpp
#pragma once

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

#define DEFAULT 8081

using namespace std;

class UdpServer

  public:
    UdpServer(string _ip, int _port = DEFAULT)
    
      ip = _ip;
      port = _port;
      sockfd = -1;
    

	//初始化服务器
    bool InitUdpServer()//返回值判断是否有错误
    
      //创建套接字
      //Ipv4用AF_INET
      //tcp协议用SOCK_DGRAM
      sockfd = socket(AF_INET, SOCK_DGRAM, 0);
      if(sockfd < 0)//创建失败就打印并返回false
      
        cerr << "socket error" << endl;
        return false;
      

      //创建套接字成功
      cout << "socket create success, sockfd : " << sockfd << endl;

      //用sockaddr族中的sockaddr_in来绑定(bind)
      struct sockaddr_in local;
      memset(&local, '\\0', sizeof(local));
      
      //该结构体共有三个成员变量
      //1.sin_family
      //以AF_INET为例
      local.sin_family = AF_INET;
      
      //2.sin_port
      //port需要发送到网络上,以保证别的主机也能找到
      //所以在传入时需要保证网络字节序
      //又由于port只需要用到低16位,所以函数名的最后一个字母是s,而不是l
      local.sin_port = htons(port);
      
      //3.sin_addr
      //sin_addr结构体内只含一个参数s_addr
      //这是ip地址的整数ip,但传入的ip一般是点分十进制表示的字符串
      //所以需要用inet函数来将其转化,但这个函数需要的参数是char*,所以用传入c_str()
      local.sin_addr.s_addr = inet_addr(ip.c_str());

      //绑定端口号
      if(bind(sockfd, (const struct sockaddr*)&local, sizeof(local)) < 0)//返回值小于0说明有错误,返回false
      
        cerr << "bind error" << endl;
        return false;
      
      cout << "bind success" << endl;

      return true;
    

    #define SIZE 128
    void Start()//启动服务器
    
      char buffer[SIZE];//读取收到的内容
      for(;;)//服务器要死循环地执行功能
      
        struct sockaddr_in addr;//创建一个结构体,可以从recvfrom获取到对端主机的信息
        socklen_t len = sizeof(addr);//该结构体的大小
        //调用recvfrom函数
        ssize_t sz = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&addr, &len);
        if(sz > 0)//读取成功
        
          buffer[sz] = '\\0';
          
          //将port转化形式后得到
          int _port = ntohs(addr.sin_port);
          
          //将整数形式的ip转化为点分十进制的字符串得到
          string _ip = inet_ntoa(addr.sin_addr);
          
          cout << _ip << " : " << _port << "# " << buffer << endl;
        
        else
        
          cerr << "recvfrom error" << endl;
        
      
    

    ~UdpServer()
    
      if(sockfd >= 0)
        close(fd);
	

  private:
    string ip;
    int port;
    int sockfd;
;

2.服务器主函数

主函数主要是调用udp_server.hpp中的定义,使服务器运行起来。

#include "udp_server.hpp"

int main(int argc, char* argv[])

  //希望传入的命令行参数是两个
  //第一个是运行可执行程序
  //第二个传入端口号port
  if(argc != 2)
  
    cerr << "usage : " << argv[0] << " port" << endl;
    return 1;
  

  string ip = "127.0.0.1";//表示本主机
  int port = atoi(argv[1]);//从命令行中拿到端口号

  //new一个指针来执行功能
  UdpServer* svr = new UdpServer(ip, port);
  svr->InitUdpServer();
  svr->Start();

  return 0;


从main函数中的ip号也可看出,先在本地测试上面关于服务器的两段代码。

运行服务器,并用netstat查看当前网络的状态,相关命令行参数的含义已在下图中指明。

通过命令可以查看到,创建的服务器已经正常运行。


3.客户端实现

实现如下,与服务器的实现相近,但更简单些。

#pragma once

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

using namespace std;

class UdpClient

  public:
    UdpClient(string _ip, int _port)
    
      server_ip = _ip;
      server_port = _port;
    

    bool InitUdpClient()
    
      //Ipv4用AF_INET
      //tcp协议用SOCK_DGRAM
      sockfd = socket(AF_INET, SOCK_DGRAM, 0);//创建套接字
      if(sockfd < 0)//发生错误
      
        cerr << "socket error!" << endl;
        return false;
      
      //客户端可以不绑定,如果不绑定则自动生成一个端口号
      return true;
    

    void Start()
    
      //设置一个addr变量传入sendto函数
      struct sockaddr_in addr;
      memset(&addr, 0, sizeof(addr));
      addr.sin_family = AF_INET;
      addr.sin_port = htons(server_port);//将port转化形式后得到
      addr.sin_addr.s_addr = inet_addr(server_ip.c_str());//将整数形式的ip转化为点分十进制的字符串得到

      string msg;//输入消息
      for(;;)
      
        cout << "Please Enter# ";
        cin >> msg;
        sendto(sockfd, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&addr, sizeof(addr));//向服务器发送msg的内容
      
    

    ~UdpClient()
    
      if(sockfd >= 0)
        close(fd);
	
  private:
    int sockfd;
    string server_ip;//ip
    int server_port;//端口号
;


4.客户端主函数

主函数如下,逻辑简单。

#include "udp_client.hpp"

int main(int argc, char* argv[])

  //要求传入的命令行参数是:可执行程序,IP地址,端口号共3个参数
  if(argc != 3)
  
    cerr << "usage : " << argv[0] << " server_ip server_port" << endl;
    return 1;
  

  string ip = argv[1];//第二个命令行参数是IP地址
  int port = atoi(argv[2]);//第三个命令行参数是服务器的端口号

  UdpClient* ucl = new UdpClient(ip, port);
  ucl->InitUdpClient();//初始化
  ucl->Start();//启动客户端
  
  return 0;


5.运行程序

运行服务器和客户端,从客户端向服务器发送消息,服务器成功收到了消息。

再次运行客户端和服务器,并用netstat查看网络状态如下,从端口号及进程名称可以看到都是完全匹配的。


四、简单的TCP网络程序

1.函数

tcp是面向链接的,也就是说在发送数据前,需要先建立链接。

(1)listen

既然需要建立链接,服务器就必须不断花时间检测是否有新的链接需要建立,这里就用到函数listen。

listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,它的值不会太大(一般是5),backlog是全链接队列的最大长度,之后会具体讲到。

调用成功返回0,失败则返回-1。


(2)accept

建立链接后当然就需要接收链接,可以通过accept函数来接收。

sockfd代表从哪里获取链接,从后面两个参数可以拿到想要链接的客户端的信息。


从返回值可以看出,成功时同样返回一个文件描述符。

第一个参数sockfd和返回值都是文件描述符,但不同的是,sockfd的作用是获取新的链接,返回值是用来服务客户端的文件描述符。


(3)connect

服务器创建链接并持续监听后,就需要客户端来连接服务器的链接了,这里要用到connect函数。


sockfd表示通过这个套接字向对端发起链接请求,对端的信息用addr和addrlen来表示。

返回值:链接或绑定成功返回0,否则返回-1。


2.服务器实现

tcp服务器的实现大体上和udp相同,区别在于tcp需要建立链接、接收链接。

而且在用tcp套接字编程时,读、写都是向同一个文件描述符进行。

#pragma once

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

using namespace std;

//下面两个宏的值可以修改为其它
#define BACKLOG 5//默认全链接队列的最大长度
#define DFL_PORT 8081//默认端口号

class TcpServer

  private:
    int port;
    int listen_sockfd;
  public:
    TcpServer(int _port = DFL_PORT)
    
      port = _port;
      listen_sockfd = -1;
    

    bool InitTcpServer()
    
      //Ipv4用AF_INET                       
      //tcp协议用SOCK_STREAM
      listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); 
      if(listen_sockfd < 0)
      
        //创建套接字失败
        cerr << "socket error" << endl;
        exit(2);
      
     
      //创建结构体
      struct sockaddr_in addr;
      memset(&addr, 0, sizeof(addr));
      //初始化成员变量
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = INADDR_ANY;//只要是发送给服务器的IP地址都要

      if(bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
      
        //绑定失败
        cerr << "bind error" << endl;
        exit(3);
      

      //这里用tcp实现,所以比udp多一步链接
      if(listen(listen_sockfd, BACKLOG) < 0)
      
        //监听失败(建立链接失败)
        cerr << "listen error" << endl;
        exit(4);
      

      return true;
    
      
    void Start()
    
      struct sockaddr_in addr;//获得客户端的信息
      for(;;)//死循环地执行
      
        socklen_t len = sizeof(addr);
        int sock = accept(listen_sockfd, (struct sockaddr*)&addr, &len);//接收链接
        if(sock < 0)
        
          //接收链接失败,继续持续接受链接
          cout << "accept error, continue ..." <<endl;
          continue;
        

        //inet_ntoa:把网络序列转成主机序列,并接着转化成点分十进制格式
        string _ip = inet_ntoa(addr.sin_addr);
        //ntohs:将客户端的端口号转化为主机序列
        int _port = ntohs(addr.sin_port);
        cout << "get a new link [" << _ip << "]:" << _port << endl;
        Service(sock, _ip, _port);
      
    

    //服务器的代码就是从sock中读入客户端发送的内容并打印
    void Service(int sock, string& _ip, int _port)
    
      while(true)
      
        char buffer[1024];
        //read的返回值
        //1.大于0表示实际读到了多少字节
        //2.等于0表示读取到文件末尾,或写端关闭
        //3.为-1表示读取出错
        ssize_t sz = read(sock, buffer, sizeof(buffer) - 1);
        if(sz > 0)
        
          buffer[sz] = '\\0';
          cout <<  _ip 学号:201621123032 《Java程序设计》第13周学习总结

2019-2020-1学期 自己8位学号 《网络空间安全专业导论》第二周学习心得

软工网络15结对编程练习(201521123007谭燕)

生成对抗网络发展研究综述

团队-团队编程项目作业名称-团队一阶段互评

Python网络编程(小白一看就懂)