Network Programming_hqck

欢迎关注我的微信公众号【万能的小江江】

网络基础

TCP协议分为两个不同的协议

  • 用来检测网络传输中差错的传输控制协议TCP(可靠传输)
  • 专门负责对不同网络进行互联的的互联网协议IP(不可靠传输)

网络采用分层的思想

  • 每一层实现不同的功能,对上层的数据做透明传输
  • 每一层向上层提供服务,同时使用下层提供的服务

OSI七层模型

OSI七层模型

  • OSI模型相关的协议目前比较少使用了,但是模型本身非常通用
  • OSI模型是一个理想化的模型,尚未有完整实现

交换机

  • 二层交换机(数据链路层)
  • 三层交换机(网络层,比较偏软件)

TCP/IP协议族的体系结构

​ TCP/IP协议是Internet事实上的工业标准

四层

  • 底下三层属于Linux Kernel
  • 应用层属于应用空间

​ 屏蔽硬件差异

​ (每种外设会有不同的驱动,往上层走就会有统一的接口)

MAC地址

​ 48位(12个数,每个数4位)的全球唯一的地址,是网络设备身份的标识

ARP/RARP协议

ARP:通过IP地址找到MAC地址

RARP:通过MAC地址找到IP地址

PPP协议

​ 是一种拨号协议(GPRS/3G/4G,传输介质不同了)

网络层(IP层,Internet Layer)

​ 端到端的传输

IP协议

IP:Internet Protocol(分为IPV4IPV6

ICMP:Internet控制管理协议,ping命令``traceroute命令(Windows下tracertpathping)属于ICMP

IGMP:Internet分组管理协议,广播、组播

传输层(Transport Layer)

​ 数据应该交给哪个任务去处理

TCP协议的特点

​ Transfer Control Protocol,传输控制协议。提供面向连接的,一对一的可靠数据传输的协议(传输过程中会占用一些资源)

​ 即数据无误、数据无丢失、数据无失序、数据无重复到达的通信

适用情况
  1. 适合对于传输质量要求较高,传输大量数据的通信
  2. 在需要可靠数据传输的场合,通常使用TCP协议
  3. WeChat/QQ等即时通讯软件的用户登陆账户管理相关的功能通常采用TCP协议
UDP协议的特点

​ User Datagram Protocol,用户数据报协议。提供不可靠,无连接的尽力传输协议(在数据发送前不需要连接,实时性会更好)

适用情况
  1. 发送小尺寸数据(对DNS服务器进行IP地址查询的时候)
  2. 接收到数据时给出应答困难的网络中使用UDP(如:无线网络)
  3. 适用于广播/组播式通信中
  4. WeChat/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
  5. 流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常可以采用UDP的方式进行实时的数据传输
SCTP协议

​ 可靠传输,是TCP的增强版,能实现多主机、多链路的通信,编程方法和TCP不太一样

为什么可靠性比TCP高

​ 它是多主机多链路传输,数据的存活性更高,可以防止传输过程中因为单条链路中断而导致数据传输的中断

应用层(Application Layer)

网页访问协议

HTTP/HTTPS

邮件发送、接收协议

POP3(收)/SMTP(发)

IMAP(可接受邮件的一部分,没有把邮件下载下来,但是也可以处理这个邮件)

文件传输协议

FTP

远程登录协议

Telnet(明文传输)/SSH(加密传输)

嵌入式相关协议
网络时钟协议

NTP

简单网络管理协议

​ 实现对网络设备集中式管理(路由器统一管理)

SNMP(开源的,搜SNMPGet)

用传输音视频的协议

​ 安防监控

RTP/RTSP(这两个协议是基于TCP+UDP实现的)

互联网协议套组

Internet Protocol Suite

TCP/IP协议通信模型

TCP/IP协议通信模型

TCP/IP协议下的数据包(封包拆包)

数据包

​ 以字节为单位

​ 传输的时候是一个封包、拆包的过程

​ 封包的时候从应用层开始,拆包的时候从链路层开始

MTU

​ Maximum Transmission Unit 最大传输单元

​ 主要和网络的类型相关,以太网的MTU是1500

MSS

​ Maximum Segment Size 最大报文长度,最大分段大小(是一个静态的数据,一般指的是一个线路数据的大小)

​ 是TCP协议定义的一个选项,MSS选项用于在TCP连接建立时,收发双方协商通信是每个报文段所能承载的最大数据长度

​ 和网络类型、线路、系统特性相关(线路不同MSS也会不同)

​ 一般是1460

TCP/IP网络编程预备知识

​ 网络里面的通信是由IP地址+端口号来决定的

Socket

​ Socket可以跨过传输层(比如ICMP的ping命令,就只通过网络层,不通过传输层,如SOCK_RAW)

Socket简介
  • 是一个应用编程接口
  • 是一种特殊的文件描述符 (everything in Unix is a file)(可以对它执行IO的操作函数,比如read(),write(),close()等操作函数)
  • 代表着网络编程的一种资源
  • 并不仅限于TCP/IP协议
  • 面向连接(Transmission Control Protocol - TCP/IP)
  • 无连接(User Datagram Protocol - UDP 和Inter-network Packet Exchange - IPX)
Socket类型
  • 流式套接字(SOCK_STREAM)

    提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送且按发送顺序接受

    内设置流量控制,避免数据流淹没慢的接收方

    数据被看作是字节流,无长度限制

  • 数据报套接字(SOCK_DGRAM)
    提供无连接服务,数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能被乱序接收

  • 原始套接字(SOCK_RAW)(对应着多个协议,发送穿透了传输层

    可以对较低层次协议,如IPICMP直接访问

这个图要理解

IP地址

​ 分为IPV4IPV6

IPV4:采用32位的整数(二进制)来表示

IPV6:采用128位的整数(二进制)来表示

MobileIPV6:包括local IP(本地注册的IP),roam IP(漫游IP)

IPV4地址

​ 点分形式:192.168.1.1

​ 32位整数(二进制):11000000 10101000 00000001 00000001

特殊IP地址

​ 局域网IP:192.xxx.xxx.xxx 10.xxx.xxx.xxx

​ 广播IP:xxx.xxx.xxx.255 255.255.255.255(全网广播)

​ 组播IP:224.xxx.xxx.xxx ~ 239.xxx.xxx.xxx

​ 子网掩码:区分网段

端口号

​ 16位的数字(1 ~ 65535)

​ 为了区分一台主机接收到的数据包应该转交给哪个任务来处理,使用端口来区别

TCP和UDP的端口号独立

管理

​ 端口号一般由IANA(Internet Assigned Numbers Authority)管理

​ 众所周知端口:1 ~ 1023(FTP:21 SSH:22 HTTP :80 HTTPS:443)(1 ~ 255之间为众所周知端口,256 ~ 1023端口通常由Unix系统占用)

​ 保留端口:1024 ~ 5000(不建议使用)

可使用端口

​ 注册端口:5000 ~ 49151

​ 动态或私有端口:49152 ~ 65535(没有端口可以被正式地注册占用)

字节序

  • 在不同CPU访问内存时,内存存储多字节数据有两种方法,称为主机字节序(HBO)(CPU访问的如果是字符串,则不存在大小端问题)

    • 小端序(little - endian)低序字节存储在低地址

      将低字节存储在起始地址,称为Little-Endian字节序,Intel和AMD,ARM等就是采用这种方式

    • 大端序(big - endian) 高序字节存储在低地址

      将高字节存储在起始地址,称为Big-Endian字节序,由ARM作为路由器、Motorola、powerpc、mips等采用

  • 网络传输中的数据必须按网络字节序,即大端字节序

  • 在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;当应用进程从socket取出整数后,要转化成小端字节序

字节序转换函数
  • 把给定系统采用的字节序称为主机字节序,为了避免不同类别的主机之间在数据交换时由于对字节序的不同而导致差错,引入了网络字节序

  • 主机字节序到网络字节序

    htonl (host to network long)

    htons (host to network short)

    • u_long htonl (u_long hostlong 4字节);
    • u_short htons (u_short short 2字节);
  • 网络字节序到主机字节序

    ntohl (network to host long)

    ntohs (network to host short)

    • u_long ntohl (u_long hostlong);
    • u_short ntohs (u_short short);
IP地址的转换
  • inet_addr()

    可以man inet_addr查看函数用法

    功能同上,返回转换后的IPV4地址

    int_addr_t inet_addr(const char *cp);

    cp:点分形式的IP地址,结果是32位整数(内部包含了字节序的转换)

    特点:

    1. 仅适用于IPV4
    2. 当初出错时返回-1
    3. 这个函数不能用于255.255.255.255的转换(涉及到补码)
  • inet_pton()

    #include <arpa/inet.h>
    
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

    将IPV4/IPV6的地址转换成binary格式(二进制)

    int iner_pton(int af, const char src, void dst);

    特点:

    1. 适用于IPV4/IPV6
    2. 能正确处理255.255.255.255的转换问题

    参数:

    1. af:地址协议族(AF_INET或AF_INET6)
    2. src:是一个指针(填写点分形式的IP地址[主要指IPV4])
    3. dst:转换的结果到dst
  • inet_ntop()

    #include <arpa/inet.h>
    
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

    把IPV4/V6的网络字节序地址编程本地字符串形式的IP地址

    特点:

    1. 适用于IPV4/IPV6
    2. 能正确处理255.255.255.255的转换问题

    参数:

    1. af:地址协议族(AF_INET或AF_INET6)
    2. src:是一个指针(填写32位的网络字节序IP地址)
    3. dst:输出结果填写点分形式的IP地址[主要指IPV4]

    返回值:

    ​ 成功返回非空指针,空指针表示有错误

TCP编程

试着自己写一下这个程序,理解read为什么也会阻塞,通过进程或者线程如何进行并发

C/S模式

网络编程的5个函数(TCP编程API)

  1. socket()函数

    man socket

    SYNOPSIS
           #include           /* See NOTES */
           #include 
    
           int socket(int domain, int type, int protocol);
    参数:
    1. domain:

      #v4或者v6的
      AF_INET      IPv4 Internet protocols    ip(7)
      AF_INET6     IPv6 Internet protocols    ipv6(7)
      #本地通信(进程间通信,两个一样的)
      AF_UNIX      Local communication        
      AF_LOCAL     Synonym for AF_UNIX    unix(7)
      #内核和用户界面通信
      AF_NETLINK    Kernel user interface device    netlink(7)
      AF_PACKET    Low level packet interface    packet(7)
    2. type:

      SOCK_STREAM:流式套接字 唯一对应于TCP

      SOCK_DGRAM:数据报套接字 唯一对应着UDP

      SOCK_RAW:原始套接字

    3. protocol:一般填0 原始套接字编程时需要填充

    返回值:

    ​ RETURN VALUE

    On sucess,a file descriptor for the new socket is returned.On error,-1 is returned,and errno is set appropriately.
    #成功时返回文件描述符,出错时返回-1
  2. bind()函数

    (底下三个参数是到时候要填充的,但是我还是有点不太理解填充在哪)

    sockfd:通过socket()函数拿到的fd

    addr:struct sockaddr的结构体变量的地址

    addrlen:地址长度

    通用结构体

    基于Internet通信的结构体(通过man7 socket命令获得)

    /* 绑定函数示例代码,IPV4 */
    /* 如果是IPV6的编程,要使用struct sockaddr_in6结构体(详细情况查看man 7 ipv6),通常更通用的方法可以用struct sockaddr_storage来编程*/
    #define SERV_PORT 5001
    #define SERV_IP_ADDR "本机ip地址"
    int main(void)
    {
        int fd = -1;
        struct sockaddr_in_sin;
        /* 1.创建socket fd 创建套接字*/
        if(fd = socket(AF_INET,SOCK_STREAM,0)) < 0{
            perror("socket");
            exit(1);
        };
        /* 2.绑定 */
        /* 2.1 填充struct sockaddr_in结构体变量 */
        bzero(&sin,sizeof(sin));
        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERV_PORT); //网络字节序的端口号
        /* 方式1:只适用于ipv4 */
        sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR); //结构体里面那个结构体,这里是把点分形式的ip地址转换成32位整数 inet_addr(const char *p)
        /* 方式2:*/
      if(inet_pton(AF_INET,SERV_IP_ADDR,(void *强制转换)sin.sin_addr.s_addr) != 1){
           perror("inet_pton");
           exit(1);
        }; //inet_pton,成功返回1,没有有效字符串地址返回0或者-1  
        /* 2.2绑定 */
        if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){
            perror("bind");
            exit(1);
        };
        /* 3.调用listen()把主动套接字变成被动套接字 */
        /* 4.阻塞等待客户端的连接请求 */
        /* 5.读写 */
        return 0;
    }
  3. listen()函数

     /* 3.调用listen()把主动套接字变成被动套接字,改变fd属性 */
    if(listen(fd,BACKLOG) < 0){
        perror("listen");
        exit(1);
    }
    //出错的时候所占用的资源会被释放,前面要先define BACKLOG
    int listen(int sockfd,int backlog); //backlog一般填5
    参数:
    1. sockfd:通过socket()函数拿到的fd
    2. backlog:同时允许几路客户端和服务器进行正在连接的过程(测试得知,ARM最大为8)
    其他:

    内核中服务器的套接字fd会维护2个链表:

    1. 正在3次握手的客户端链表(数量 = 2 * backlog + 1,如果是5的话就是11路)
    2. 已经建立好连接的客户端链表(已经完成3次握手分配好了newfd)

    比如:listen(fd,5); // 表示系统允许11 = (2*5+1)个客户端同时进行三次握手

    返回值:

    ​ 成功返回0,出错返回-1

  4. accept()函数

     /* 4.阻塞等待客户端的连接请求,因为是个系统应用?用man 2 accept查手册 */
    int accept(int sockfd,struct sicjaddr *addr,socklen_t *addrlen); //sockfd是被前面的处理过的,后面两个变量会取到客户端的信息(可以获取到IP地址和端口号((前提是传入的数据有相关信息)
    newfd = accept(fd,NUll,NULL);
    if(newfd < 0){
        perrot("accept");
        exit(1);
    }
    #define QUIT_STR "quit"
    #include <unistd.h>
    #include <stdlib.h>
    #incldue <strings.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <errno.h> //与errno相关
    /* 5.读写 */
     //..和newfd进行数据读写
     //...FIXME!!!(已知无法执行的代码的意思)
    int ret = -1;
    char buf[BUFSIZ];
    while(1){
        bzero(buf, BUFSIZ);
        do{
            read(newfd, buf, BUFSIZ);
        }while(ret < 0 && EINTR == errno);
        if (ret < 0){
            perror("read");
            exit(1);
        }
        if (ret == 0){ //对方已关闭
            break;
        }
        printf("Receive data: %s\n",buf);
        if (strncasecmp(buf, QUIT_STR, strlen(QUIT_STR)){
            //用户输入了quit子政府
            printf("Client is exiting!\n");
            break;
        }
    }
    close(newfd);
    close(fd);
    return 0;

    man strcmp字符串比较函数

    参数:
    1. sockfd:经过前面socket()创建并通过bind()``,listen()设置过的fd
    2. addraddrlen
    返回值:

    成功时返回已经建立好连接的新的newfd,出错的时候返回-1(可以有很多个newfd,看建立多少个连接,newfd 1 2 3 …)

  5. connect()函数

    连接函数

    SYNOPSIS
        #include <sys/types.h>    //See Notes
        #include <sys/socket.h>
    
        int connect(int sockfd,const struct sockaddr *addr, socklen_t addrlen);
    if(connect(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){
        perror("connect");
        exit(1);
    }

    connec()函数和bind()函数的写法类似

    参数:
    1. sockfd:通过socket()函数拿到的fd
    2. addrstruct sockaddr的结构体变量的地址
    3. addrlen:地址长度
    返回值:

    ​ 连接成功返回0,出错返回-1

    /* net.h */
    /* 把常用头文件放到这里 */
    #ifndef __MAKEU_NET_H__
    #define __MAKEU_NET_H__
    
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #incldue <strings.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <errno.h>
    
    #define SERV_PORT 5001
    #define SERV_IP_ADDR "本机IP"
    #define BACKLOG 5
    
    #define QUIT_STR "quit"
    
    #endif
/* 总的程序 */
/* 需要实现在client(客户端)发送数,在sever(服务端)可以实时显示接收数据 */
#include "net.h"

int main(void)
{
    int fd = -1;
    struct sockaddr_in sin;
    /* 1.创建socket_fd 创建套接字 */
    if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        perror("socket");
        exit(1);
    }

    /* 2.绑定 */
    /* 2.1 填充struct sockaddr_in结构体变量 */
    bzero(&sin,sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SERV_PORT); //网络字节序的端口好

    /* 优化1:让服务器程序能绑定在任意的IP上 */
#if 1
    sin.sin_addr.s_addr = htonl(INADDY_ANY); //INADDY_ANY是-1,在大端小端模式下是一样的
#else
  if(inet_pton(AF_INET,SERV_IP_aDDR,(void *)&sin.sin_addr) != 1){
      perror("inet_pton");
      exit(1);
  }
}
#endif
    /* 2.2 绑定 */
    if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0){
        perror("bind");
        exit(1);
    }
    /* 3.调用listen()把主动套接字变成被动套接字 */
    if(listen(fd,BACKLOG) < 0){
        perror("listen");
        exit(1);
    }
    printf("Server starting...OK!\n");
    int newfd = -1;
    /* 4.阻塞等待客户端连接请求 */
#if 0
    newfd = accept(fd,NULL,NULL);
    if(newfd < 0){
        perror("accept");
        exit(1);
    }
#else
    /* 优化2:通过程序获取刚建立连接的socket的客户端IP地址和端口号 */
    struct sockaddr_in cin;
    socklen_t addrlen = sizeof(cin);
    if((newfd = accept(fd,(struct sockaddr *)&cin,&addrlen)) < 0){
        perror("accept");
        exit(1);
    }
    char ipv4_addr[16];
    if(!inet_ntop(AF_INET,(void *)cin.sin_addr,ipv4_addr,sizeof(cin))){
        perror("inet_ntop");
        exit(1);
    }

    printf("Client(%s:%d) is connected!\n",ipv4_addr,ntons(cin.sin_port));
#endif
    /* 5.读写 */
    //..和newfd进行数据读写
    int ret = -1;
    char buf[BUFSIZ];
    while(1){
        bzero(buf,BUFSIZ);
        do{
            ret = read(newfd,buf,BUFSIZE-1);
        }while(ret < 0 && EINTR == errno);
        if(ret < 0){
            perror("read");
            exit(1);
        }
    }

并发服务器

​ 可以通过多线程或者多进程的方式来做(旧的服务器不能并发的原因:accept()函数被阻塞,read()函数阻塞)

​ 所以要在accept的地方进行一个优化

三种方式的区别

这个课程感觉不够系统,之后的笔记就不继续更新了


   转载规则


《Network Programming_hqck》 InImpasse 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录