首页 > 编程语言 >网络编程学习笔记

网络编程学习笔记

时间:2024-12-26 09:29:38浏览次数:5  
标签:addr int 编程 网络 笔记 sockfd include buf 客户端

1.网络编程

1.1.介绍(IO)

怎么学:理解(应用层)、多回顾、多练、自主

要求:互动、认真听、互相尊重

1.1.1.认识网络

网络:多设备通信

认识网络

1.2.IP地址

1.2.1.基本概念

1. IP地址是Internet中主机的标识

2. Internet中的主机要与别的机器通信必须具有一个IP地址

3. IP地址为32位(IPv4)或者128位(IPv6)

NAT:公网转私网、私网转公网

4. IPV4表示形式:常用点分十进制形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。

1.2.2.NAT设备(网络地址转换)

功能:NAT设备的主要功能是将内部私有IP地址转换为公共IP地址,或反向操作,以便在局域网和外部网络(如互联网)之间进行通信。

● 工作原理:

当局域网内的设备访问互联网时,NAT设备记录设备的内部IP地址和源端口,并用公共IP地址替换。

响应的流量也会经过NAT设备,NAT通过记录的映射关系将数据包发送回正确的内部设备。

● 作用对象:NAT设备用于管理与外部网络(如互联网)的通信,通常作为路由器的一部分。

1.2.3.DHCP服务器(动态主机配置协议)

功能:DHCP服务器的主要任务是自动分配IP地址、子网掩码、默认网关和DNS服务器ip等网络配置给网络中的设备(客户端),简化网络管理。

工作原理

当一个设备(如电脑或手机)首次连接到网络时,它会发送一个DHCP请求。

DHCP服务器接收到请求后,从预设的IP地址池中分配一个可用的IP地址,并返回给客户端。

作用对象:DHCP服务器专注于局域网内部的IP地址管理。

1.2.4.网络号/主机号(二级划分)

思考:你了解你的身份证号吗?

IP地址 = 网络号+主机号

网络号:表示是否在一个网段内(局域网)

主机号:标识在本网段内的ID,同一局域网不能重复

1.2.2.IP地址分类

1.2.2.1.整体分类

1.2.2.1.1.A类:
(0.0.0.0-127.255.255.255)(默认子网掩码:255.0.0.0)

第一个字节为网络号,后三个字节为主机号(一个字节占8位)。该类IP地址的最前面为“0”,因为网络号是8位,所以地址的网络号取值于 0~127之间(0000 0000)(0111 1111)。

一般用于大型网络,主机号取值在0 - 2 ^24之间. 127.0.0.1:本机回环地址

1.2.2.1.2.B类:

(128.0.0.0-191.255.255.255)(默认子网掩码:255.255.0.0)

前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。

一般用于中等规模网络。

1.2.2.1.3.C类:

(192.0.0.0-223.255.255.255)子网掩码:255.255.255.0)

前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。

一般用于小型网络(一般教室使用的是C类)。

1.2.2.1.4.D类:

(224.0.0.0- 239.255.255.255)是多播地址。

该类IP地址的最前面为“1110”,所以地址的范围取值于224~239之间。

一般用于组播用户,组播通信。

E类:是保留地址。该类IP地址的最前面为“1111”,所以地址的取值于240~247之间。

一般是一些保密单位用得到,平时基本不会用到

A类:1.0.0.1~126.255.255.254

B类:128.0.0.1~~191.255.255.254

C类:192.0.0.1~~223.255.255.254

D类(组播地址):224.0.0.1~~239.255.255.254

1.2.2.2.特殊地址

0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。

127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。

网络地址: 每一个网段主机号为0的地址; 如: 192.168.50.169--》192.168.50.0 它是网络中的一个特殊地址,不能被分配给任何具体的主机。

广播地址: 主机号最大的地址是该网段的广播地址 如:192.168.50.255

如: b类IP 广播地址为: 130.223.255.255

全网广播地址 : 255.255.255.255, 该地址不能随便用,会影响这个网络

  1. 子网掩码
  1. 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址
  2. 子网掩码长度是和IP地址长度完全一样;
  3. 网络号全为1,主机号全为0
  4. 公式:网络地址=IP & MASK(子网掩码)

主机地址= IP& ~MASK

子网掩码 . . . . . . . . . . . . : 255.255.255.0

IP. . . . . . . . . . . . . . . :192.168.50.236

网络地址:192.168.50.236 & 255.255.255.0===》192.168.50.0

主机地址:192.168.50.236 & ~255.255.255.0===》192.168.50.236 & 0.0.0.255==》0.0.0.236

  1. 练习

练习一:B类地址的子网掩码怎么写?255.255.0.0

练习二:B类地址,同一网段最多可以连接多少个主机?216-2

练习三:已知一个子网掩码号为255.255.255.192,问:最多可以连接多少台主机?子网掩码:主机号全为0,网络号全为1

192==》1100 0000 26-2

练习四:一个IP地址为192.168.3.183,计算其网络号与主机号

网络号:192.168.3.0

主机号:0.0.0.183

练习五: 如果有800台电脑, 在不浪费ip情况下, 选用哪个网段? B

1.2.2.3.三级划分

作用: 重新划分网络号和主机号 , 也就是重新组网 , 从而提高资源利用率

二级划分 IP :IP地址= 网络号 + 主机号

三级划分 IP :IP地址= 网络号 + 子网号 + 主机号

笔试1:某公司有四个部门:行政、研发1、研发2、营销,每个部门各50台计算机接入公司局域网,如果要在192.168.1.0网段为每个部门划分子网,子网掩码应该怎么设置,每个子网的地址范围分别是什么?(4个部门之间不能通信)

C类:254

192.168.1.0000 0000

行政:192.168.1.00 00 0000-192.168.1.00 11 1111==》192.168.1.0-192.168.1.63

研发1:192.168.1.01 00 0000-192.168.1.01 11 1111=》192.168.1.64-192.168.1.127

研发2:192.168.1.10 000000-192.168.1.10 11 1111=》192.168.1.128-192.168.1.191

营销:192.168.1.11 00 0000-192.168.1.11 11 1111=》192.168.1.192.-192.168.1.255

子网掩码:255.255.255.11 00 0000==》255.255.255.192

有两台电脑主机,在最少浪费IP地址的情况下.将172.16.14.4与172.16.13.2划归为同一网段,则子网掩码应该设置为

172.16.14.4==>172.16.0000 1110.4

172.16.13.2==>172.16.0000 1101.2

子网掩码:255.255.1111 11 00.0==》255.255.252.0

三级划分:网络号(172.16)+子网号(二进制0000 11)+主机号(各自的主机号)

2.网编函数

2.1套接字

  1. socket发展

1)1982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口

2)1986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信

3)现在很多应用 (FTP, Telnet) 都依赖这一接口

  1. socket介绍

1、是一个编程接口

2、是一种特殊的文件描述符 (everything in Unix is a file)

3、socket是一种通信机制,并不仅限于TCP/IP协议

4、面向连接 (Transmission Control Protocol - TCP/IP)

5、无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)

  1. 为什么需要socket

1.普通的I/O操作过程 :打开文件->读/写操作->关闭文件

2.TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作 ----->进行网络通信的两个进程在不同的机器上,如何连接? 网络协议具有多样性,如何进行统一的操作 ?

需要一种通用的网络编程接口:Socket

  1. socket类型

流式套接字(SOCK_STREAM) ---> TCP

提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流

数据报套接字(SOCK_DGRAM) --> UDP

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

原始套接字(SOCK_RAW)

可以对较低层次协议如IP、ICMP直接访问,还有一些ping命令

  1. 位置

2.2.端口号

● 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分

● TCP端口号与UDP端口号独立(UDP port为8888,TCP port也可为8888 )

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

● 端口用两个字节来表示

众所周知端口(被占用:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用))

已登记端口:1024~49151(----可用来建立与其它主机的会话----)

动态或私有端口:49152~65535 --固定某些服务使用--

2.3.字节序

字节序: 不同类型的cpu主机,内存存储大于一个字节类型的数据在内存中的存放顺序

分类:

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

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

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

终端显示的数据必须是主机字节序,即小端字节序

网络传输中,需要将每个主机的主机字节序(CPU决定),转换为网络中统一顺序的网络字节序,才能供双方主机去识别。

只需要转换IP和port就可以,不需要转换传输的数据包的字节序,因为IP和port为 4个字节和2个字节, 而数据报一般都为char类型, 占一个字节,根据字节序的性质,内存存储大于一个字节类型的数据在内存中的存放顺序,所以char类型并不具有字节序的概念。

面试题:写一个程序,判断当前主机的字节序?

测试方法:共用体 数据类型强转、指针强转

2.3.1.端口转换 5678

2.3.1.1.主机字节序转换为网络字节序 (小端序->大端序)

u_long htonl (u_long hostlong); //host to internet long

u_short htons (u_short short);  //掌握这个

2.3.1.2.网络字节序转换为主机字节序(大端序->小端序)

u_long ntohl (u_long hostlong);

u_short ntohs (u_short short);

2.3.2.IP地址转换 "192.168.31.238"

2.3.2.1.主机字节序转换为网络字节序 (小端序->大端序)

in_addr_t  inet_addr(const char *strptr);  //该参数是字符串
typedef uint32_t in_addr_t;
struct in_addr {
    in_addr_t s_addr;
};
功能:  主机字节序转为网络字节序
参数:  const char *strptr: 字符串
返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示), 
      否则NULL

2.3.2.2.网络字节序转换为主机字节序(大端序->小端序)

char *inet_ntoa(stuct in_addr inaddr);
功能:   将网络字节序二进制地址转换成主机字节序。 
参数:  stuct in_addr in addr  : 只需传入一个结构体变量
返回值:  返回一个字符指针, 否则NULL;

3.TCP编程

C/S:客户端、服务器 B/S: 浏览器、服务器

3.1.流程

服务器:------------------------------》接电话者

  1. 创建套接字(socket)---------------------------》有手机
  2. 指定网络信息---------------------------------------》有号码(ifconfig)windosw-(ifconfig)
  3. 绑定套接字(bind)------------------------------》绑定手机(插卡)
  4. 监听套接字(listen)----------------------------》待机
  5. 接收客户端连接请求(accept)--------------》接电话
  6. 接收、发送数据(recv send)----------------》通话
  7. 关闭套接字(close)-----------------------------》挂断电话

客户端:------------------------------》打电话者

  1. 创建套接字(socket)---------------------------》有手机
  2. 指定(服务器)网络信息----------------------》有对方号码(ifconfig查看ip地址)
  3. 请求连接(connect)---------------------------》打电话
  4. 接收、发送数据(recv send)---------------》通话
  5. 关闭套接字(close)-----------------------------》挂断电话

3.1.函数接口

3.1.1.创建套接字socket

int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
   domain:协议族
     AF_UNIX, AF_LOCAL  本地通信
     AF_INET            ipv4
     AF_INET6            ipv6
  type:套接字类型
     SOCK_STREAM:流式套接字
     SOCK_DGRAM:数据报套接字
      SOCK_RAW:原始套接字
  protocol:协议 - 填0 自动匹配底层 ,根据type
  系统默认自动帮助匹配对应协议
     传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
     网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
 返回值:
    成功 文件描述符
    失败 -1,更新errno

3.1.2.绑定套接字bind

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:绑定
参数:
    socket:套接字
    addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,
  填充对应结构体-通信当时socket第一个参数确定)   
    addrlen:结构体大小   
  返回值:成功 0   失败-1,更新errno
  
 通用结构体:
  struct sockaddr {
     sa_family_t sa_family;
     char        sa_data[14];
 }

ipv4通信结构体:
struct sockaddr_in {
    sa_family_t    sin_family;   //创建的套接字
    in_port_t      sin_port;     //
    struct in_addr sin_addr;  
};
struct in_addr {
    uint32_t       s_addr;    
};

本地通信结构体:
 struct sockaddr_un {
     sa_family_t sun_family;               /* AF_UNIX */
     char        sun_path[108];            /* pathname */
 };

3.1.3.监听listen

int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字
参数:
 sockfd:套接字
 backlog:同时响应客户端请求链接的最大个数,不能写0.
  不同平台可同时链接的数不同,一般写6-8个
    (队列1:保存正在连接)
    (队列2,连接上的客户端)
   返回值:成功 0   失败-1,更新errno 

3.1.4.等待客户端链接accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
功能:阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
   Sockfd :套接字
   addr: 链接客户端的ip和端口号
      如果不需要关心具体是哪一个客户端,那么可以填NULL;
   addrlen:结构体的大小
     如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值: 
     成功:文件描述符; //用于通信
		失败:-1,更新errno

​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​3.1.5.连接服务器  connect

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
     1、sockfd:socket函数的返回值
     2、addr:填充的结构体是服务器端的;
     3、addrlen:结构体的大小
返回值 
      -1 失败,更新errno
      正确 0 

​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​3.1.6.接收数据 recv

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据 
参数: 
    1、sockfd: 套接字;
    2、 buf  存放位置
    3、len  大小
    4、 flags  一般填0,相当于read()函数
    MSG_DONTWAIT  非阻塞
返回值: 
   < 0  失败出错  更新errno
   ==0  表示客户端退出
   >0   成功接收的字节个数

​​​​​​​​​​​​​​3.1.7.发送数据 send

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
   1、 sockfd:socket函数的返回值
   2、buf:发送内容存放的地址
   3、 len:发送内存的长度
   4、 flags:如果填0,相当于write();
返回值:   < 0 失败出错 更新errno ==0 表示客户端退出 
          >0 成功接收的字节个数





​​​​​​​

【1】TCP

  1. 服务器

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 0;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8888);
    saddr.sin_addr.s_addr = inet_addr("192.168.50.94");

    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 监听
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");
    //接收连接
    int acceptfd = accept(sockfd, NULL, NULL);
    if (acceptfd < 0)
    {
        perror("accept err");
        return -1;
    }
    printf("acceptfd:%d\n", acceptfd);
    //通信
    while (1)
    {
        ret = recv(acceptfd, buf, sizeof(buf), 0);
        if (ret < 0)
        {
            perror("recv err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("client exit\n");
            break;
        }
        else
        {
            printf("buf:%s\n", buf);
            memset(buf, 0, sizeof(buf));
        }
    }
    // 关闭套接字
    close(acceptfd);
    close(sockfd);

    return 0;
}

  1. 客户端

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 0;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8888);
    saddr.sin_addr.s_addr = inet_addr("192.168.50.94");

    // 连接
    if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("connect err");
        return -1;
    }
    printf("connect ok\n");

    // 通信
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        send(sockfd, buf, sizeof(buf), 0);
        memset(buf, 0, sizeof(buf));
    }
    // 关闭套接字
    close(sockfd);

    return 0;
}

  1. 练习
  1. 优化服务器代码,客户端链接成功后,可以循环多次通信,当客户端输入quit时,客户端退出

  1. 优化服务器代码客户端输入quit退出后,服务器不退出,等待下一个客户端连接

循环服务器:一个服务器可以连接多个客户端,但是不能同时连接

  1. 地址和端口都通过参数传入

  1. 自动获取本机地址0.0.0.0

  1. 增加来电显示功能:显示客户端连入的地址

服务器

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 0;
    int acceptfd;
    int len;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    struct sockaddr_in saddr,caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    // saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    saddr.sin_addr.s_addr = INADDR_ANY;
    len=sizeof(caddr);
    // uint32_t
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 监听
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");
    // 接收连接
    while (1)
    {
        //accept:第二个参数:结构体,存放连接者的信息
        //accept:第三个参数:地址存放结构体大小
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        printf("acceptfd:%d\n", acceptfd);
        printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr) ,ntohs(caddr.sin_port));
        // 通信
        while (1)
        {
            ret = recv(acceptfd, buf, sizeof(buf), 0);
            if (ret < 0)
            {
                perror("recv err");
                return -1;
            }
            else if (ret == 0)
            {
                printf("client exit\n");
                break;
            }
            else
            {
                printf("buf:%s\n", buf);
                memset(buf, 0, sizeof(buf));
            }
        }
        // 关闭套接字
        close(acceptfd);
    }
    close(sockfd);

    return 0;
}

客户端

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 0;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    //atoi:字符串转换位整形
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);

    // 连接
    if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("connect err");
        return -1;
    }
    printf("connect ok\n");

    // 通信
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        if (!strcmp(buf, "quit"))
            break;
        send(sockfd, buf, sizeof(buf), 0);
        memset(buf, 0, sizeof(buf));
    }
    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端依旧可以固定端口号,加结构体和bind,bind起到固定IP和端的作用

【2】网络模型

网络的体系结构

网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。

每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务

网络体系结构即指网络的层次结构和每层所使用协议的集合

两类非常重要的体系结构:OSI与TCP/IP

OSI模型---了解-----ISO国际标准化组织

OSI模型是最理想的模型

应用层:指定特定应用的协议,文件传输,文件管理,电子邮件等。

表示层:确保一个系统应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据加密,解密;

会话层:通信管理,负责建立或者断开通信连接

传输层:端口号,数据传输到具体那个进程程序(端到端)

网络层:路由器中是有算法的,ip,(主机到主机)(路由的转发)

链路层:格式变为帧(把数据分成包,一帧一帧的数据进行发送)

物理层:传输的是bit流(0与1一样的数据),物理信号,没有格式

TCP/IP模型---规范

应用层:应用协议和应用程序的集合

传输层:决定数据交给机器的哪个任务(进程)去处理,通过端口寻址

进程一定有进程号,不一定有端口号,端口号只有在进行网络通信才有

网络层:提供设备到设备的传输,可以理解为通过IP寻址机器。

网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。

OSI和TCP/IP模型对应关系图

常见协议

网络接口和物理层:

ppp:拨号协议(老式电话线上网方式)

Ethernet:是一种常见的局域网技术,使用MAC地址进行帧的传输和接收

Wi-Fi:用于无线局域网的数据传输,通常基于IEEE 802.11标准

网络层:

IP(IPV4/IPV6):网间互连的协议

ICMP:网络控制管理协议,ping命令使用

IGMP:网络分组管理协议,广播和组播使用

ARP:地址解析协议 IP-->MAC

RARP:反向地址转换协议 MAC-->IP

传输层:

TCP:传输控制协议

UDP:用户数据报协议

应用层:

SSH:加密协议

telnet:远程登录协议

FTP:文件传输协议

HTTP:超文本传输协议

DNS:域名解析协议 (将人类易于记忆的域名(如www.baidu.com)转换为计算机使用的IP地址(如192.0.2.1))

SMTP/POP3:邮件传输协议

DNS

由于使用IP地址来指定计算机不方便人们记忆,且输入时候容易出错,用字符标识网络中计算机名称方法。

这种命名方法就像每个人的名字,这就是域名(Domian Name )

域名服务器(Domain Name server):用来处理IP地址和域名之间的转换。

域名系统(Domain Name System,DNS):域名翻译成IP地址的软件DNS

域名结构

例如域名 www.baidu.com.cn 从右向左看

cn为高级域名,也叫一级域名,它通常分配给主干节点,取值为国家名,cn代表中国

com为网络名,属于二级域名,它通常表示组织或部门

中国互联网二级域名共40个,edu表示教育部门,com表示商业部门,gov表示政府,军队mil等等

baidu为机构名,在此为三级域名,表示百度

www:万维网world wide web,也叫环球信息网,是一种特殊的信息结构框架。

DNS工作流程

DNS黑客技术

网络调试命令

ping:测试网络连通性(ICMP)

作为平时网络连通检测使用最多的命令,它的作用主要为:

● 用来检测网络的连通情况和分析网络速度;

● 根据域名得到服务器IP;

● 根据ping返回的TTL值来判断对方所使用的操作系统及数据包经过路由器数量。

字节:数据包大小,也就是字节。

时间:响应时间,这个时间越小,说明你连接这个地址速度越快。

TTL:Time To Live,从源到目的,每经过一个路由器,TTL减1,当TTL=0,包丢掉

netstat

netstat是控制台命令,是一个监控TCP/IP网络的非常有用的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。Netstat用于显示与IP、TCP、UDP相关的统计数据,一般用于检验本机各端口的网络连接情况。

作用:测试网络状态
netstat -a  //查看所有网络状态
netstat -at  //查看tcp所有网络状态
netstat -au //查看udp所有网络状态
netstat -l //查看处于监听状态的链接
netstat -lt //查看处于监听状态的链接tcp
netstat -lu //查看处于监听状态的链接udp
netstat -lx //查看处于监听状态的链接unix

手机调试工具

可以直接在手机应用市场搜索"全能调试"下载,也可以通过以下网站。

全能调试软件下载-全能调试app下载v1.0.5官方版-西西软件下载

TCP/UDP

UDP TCP 协议相同点:都存在于传输层,全双工通信

TCP:全双工通信、面向连接、可靠

TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。

高可靠原因:1. 三次握手、四次挥手

  1. 序列号和应答机制
  2. 超时,错误重传机制
  3. 拥塞控制、流量控制(滑动窗口)

适用场景

适合于对传输质量要求较高的通信

在需要可靠数据传输的场合,通常使用TCP协议

MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

UDP:全双工通信、面向无连接、不可靠

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

适用场景

发送小尺寸数据(如对DNS服务器进行IP地址查询时)

适合于广播/组播式通信中。

MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议

【3】wireshark

  1. 安装

win:

关闭防火墙,安装包默认下载就可

linux:

sudo apt-get install wireshark

  1. 启动

win:

双击打开

linux:

sudo wireshark

  1. 选择网卡

win:

linux:

抓包工具的界面介绍如下:

  1. 过滤包

  1. ip.addr == x.x.x.x:只显示源或目标IP地址为x.x.x.x的数据包。
  2. tcp.port == x:只显示源或目标端口号为x的TCP数据包。
  3. udp.port == x:只显示源或目标端口号为x的UDP数据包。
  4. ip.src == x.x.x.x:只显示源IP地址为x.x.x.x的数据包。
  5. ip.dst == x.x.x.x:只显目标IP地址为x.x.x.x的数据包。

【4】三次握手与四次挥手

三次握手

第一次握手都由客户端发起

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成,称为被动打开(passive open)。

第一次握手:客户通过调用connect进行主动打开(active open)。这引起客户TCP发送一个SYN(表示同步)分节(SYN=J),它告诉服务器客户将在连接中发送数据的初始序列号。并进入SYN_SEND状态,等待服务器的确认。

第二次握手:服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV状态。

第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户服务器进入ESTABLISHED状态,完成三次握手。

  1. SYN_SEND:客户端发送SYN报文后进入此状态,等待服务器的确认。
  2. SYN_RECV‌:服务器收到SYN报文后进入此状态,等待客户端的确认。
  3. ESTABLISHED‌:当客户端和服务器端都发送和接收了ACK报文后,连接进入此状态,表示连接已经建立,可以进行数据传输。

类比打电话的过程:

第一次握手:喂,能听见我说话吧?

第二次握手:能听见你说话,你能听见我说话不?

第三次握手:能听见

开始通话

客户端的初始序列号为J,而服务器的初始序列号为K。在ACK里的确认号为发送这个ACK的一端所期待的下一个序列号。因为SYN只占一个字节的序列号空间,所以每一个SYN的ACK中的确认号都是相应的初始序列号加1.类似地,每一个FIN(表示结束)的ACK中的确认号为FIN的序列号加1.

完成三次握手,客户端与服务器开始传送数据,在上述过程中还有一些重要概念。

未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户端确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户端确认包时,删除该条目,服务器进入ESTABLISHED状态。

第一次握手:客户端发送SYN握手包(seq:a),进入等待服务器应答的状态(SYN_SEND)

第二次握手:服务器在收到客户端发送的握手包之后,给客户端回复一个ACK,还有一个握手包SYN(seq:b ack:a+1),进入等待接收的状态(SYN_RECV)

第三次握手:客户端在收到服务器发送的握手包以及确认包之后,给服务器再回复一个确认包ACK(seq:c,ack:b+1)

发送一次数据都要有序列号,但是不一定有应答号,只有这一次的数据中有应答包的时候才会有应答号

面试题-----》TCP连接过程:三次握手

---------》TCP的三次握手发生在哪两个函数之间:connect accept

---------》为什么一定是三次握手,而不能是两次握手:主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而导致不必要的错误和资源的浪费。

两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据

【1】三次握手四次挥手

四次挥手

四次挥手

四次挥手既可以由客户端发起,也可以由服务器发起

TCP连接终止需四个分节。

类比挂电话的过程:

第一次挥手:我说完了,我要挂了

第二次挥手:好的,我知道了,但是你先别急,等我把话说完

第三次挥手:好了,我说完了,咱们可以挂电话了

第四次挥手:好的,挂了吧

1MSL:数据包在系统内的最大存活时间

第一次挥手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。

第二次挥手:接收到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为文件结束符传递给接收端应用进程(放在已排队等候应用进程接收到任何其他数据之后)

第三次挥手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。

第四次挥手:接收到这个FIN的原发送端TCP对它进行确认。

第一次挥手:主动断开方向被动断开方发送FIN挥手包,表示自己发送完毕

第二次挥手:被动断开方接收到FIN之后给主动断开方回复ACK

第三次挥手:被动断开方向主动断开方发送FIN挥手包,表示自己也发送完毕。

第四次挥手:主动断开方接收到FIN之后给被动断开方回复ACK,表示确认关闭连接。

面试题-----》描述一下四次挥手

---------》第二次挥手与第三次挥手之间有时间间隔是为什么?

---------》第四次挥手之后主动断开方会等待一段时间再关闭,这个等待的时间是多少?为什么要等待? 2MSL

【2】UDP

  1. 通信流程

服务器-----------------------------》短信的接收方

  1. 创建数据报套接字(socket)---------------------------》有手机
  2. 指定网络信息-----------------------------------------------》有号码
  3. 绑定套接字(bind)--------------------------------------》插卡
  4. 接收、发送消息(recvfrom、sendto)--------------》收短信
  5. 关闭套接字(close)------------------------------------》接收完毕

客户端---------------------------》短信的发送方

  1. 创建数据报套接字(socket)---------------------------》有手机
  2. 指定(接收者)网络信息--------------------------------》有号码
  3. 接收、发送消息(recvfrom、sendto)--------------》发短信
  4. 关闭套接字(close)------------------------------------》发送完毕

  1. 函数接口

1.recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
				struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
	sockfd:套接字描述符
	buf:接收缓存区的首地址
	len:接收缓存区的大小
	flags:0
	src_addr:发送端的网络信息结构体的指针
	addrlen:发送端的网络信息结构体的大小的指针
返回值:
	成功接收的字节个数
	失败:-1
	0:客户端退出

2.sendto

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                 const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
	sockfd:套接字描述符
	buf:发送缓存区的首地址
	len:发送缓存区的大小
	flags:0
	src_addr:接收端的网络信息结构体的指针
	addrlen:接收端的网络信息结构体的大小
返回值: 
	成功发送的字节个数
	失败:-1

3. 服务器端

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    int ret;
    char buf[128]={0};
    // 1.创建数据报套接字(socket)---------------------------》有手机
    int sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd<0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n",sockfd);
    // 2.指定网络信息-----------------------------------------------》有号码
    struct sockaddr_in saddr,caddr;
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(atoi(argv[1]));
    saddr.sin_addr.s_addr=INADDR_ANY;
    int len = sizeof(caddr);

    // 3.绑定套接字(bind)--------------------------------------》插卡
    if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr))<0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind okk\n");
    // 4.接收、发送消息(recvfrom、sendto)--------------》收短信
    while(1)
    {
        //recvfrom最后两个参数决定了收到的是谁的消息
        ret=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&caddr,&len);
        if(ret<0)
        {
            perror("recv from err");
            return -1;
        }
        printf("buf:%s\n",buf);
        printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr) ,ntohs(caddr.sin_port));
        memset(buf,0,sizeof(buf));
    }
    // 5.关闭套接字(close)
        close(sockfd);
    return 0;
}

  1. 客户端

#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    int ret;
    char buf[128] = {0};
    // 1.创建数据报套接字(socket)--
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 2.指定网络信息-------
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);

    // 4.接收、发送消息(recvfrom、sendto)------
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        //sendto最后两个参数决定了要发送消息给谁
        sendto(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&saddr,sizeof(saddr) );
        memset(buf, 0, sizeof(buf));
    }
    // 5.关闭套接字(close)------------------------------------》接收完毕
    close(sockfd);
    return 0;
}

注意:

1、对于TCP是先运行服务器,客户端才能运行。

2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,

3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。

4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。

5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。

分析:(注:saddr:服务器信息 caddr:客户端信息)

  1. 客户端从终端获取字符串,发送给服务器sendto(saddr)
  2. 服务器接收客户端发来的信息recvfrom(caddr)
  3. 拼接字符串sprintf
  4. 服务器姜拼接好的字符串发送给客户端sendto(caddr)
  5. 客户端接收服务器发送来的字符串recvfrom()

From

​​​​​​​

【3】Linux IO模型

4种:阻塞IO、非阻塞IO、信号驱动IO(异步IO)、IO多路复用

  1. 阻塞式IO:最常见、效率低、不耗费CPU

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。

学习的读写函数在调用过程中会发生阻塞相关函数如下:

•读操作中的read、recv、recvfrom

读阻塞--》需要读缓冲区中有数据可读,读阻塞解除

•写操作中的write、send

写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。

注意:sendto没有写阻塞

1)无sendto函数的原因:

sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。

2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。

•其他操作:accept、connect

udp丢包

tcp粘包

tcp拆包

TCP粘包、拆包发生原因:

发生TCP粘包或拆包有很多原因,常见的几点:

1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包

2、待发送数据大于MSS(传输层的最大报文长度),将进行拆包(到网络层拆包 - id ipflags )。

3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包

4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

粘包解决办法:

解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

4、延时发送

tcp粘包与udp丢包的原因 - 清风软件测试开发 - 博客园

UDP不会造成粘包和拆包, TCP不会造成丢包

UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将数据包分隔开, 不会造成粘包。

UDP不会造成拆包, 但会出现拆包, 这个拆包是在网络层的IP头进行的拆包(判断MTU)。

TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;

TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)

为什么UDP会造成丢包:

UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。

丢包出现原因: 接收缓存区满 网络拥堵, 传输错误

2. 非阻塞IO:轮询、耗费CPU,可以处理多路IO

•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

•这种模式使用中不普遍。

设置非阻塞的方式

    1. 通过函数自带参数设置

    1. 通过设置文件名描述符的属性,把文件描述符属性设置为非阻塞

int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符属性
参数:
   fd:文件描述符
   cmd:设置方式 - 功能选择
        F_GETFL  获取文件描述符的状态信息     第三个参数化忽略
        F_SETFL  设置文件描述符的状态信息     通过第三个参数设置
        O_NONBLOCK  非阻塞
        O_ASYNC     异步
        O_SYNC      同步
  arg:设置的值  in
返回值:
      特殊选择返回特殊值 - F_GETFL  返回的状态值(int)
        其他:成功0  失败-1,更新errno
        
使用:0为例
  0-原本:阻塞、读权限  修改或添加非阻塞
  int flags=fcntl(0,F_GETFL);//1.获取文件描述符原有的属性信息
  flags = flags | O_NONBLOCK;//2.修改添加权限
  fcntl(0,F_SETFL,flags);    //3.将修改好的权限设置回去

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    // 0:标准输入
    // 1.先获取文件描述符的属性
    int flags = fcntl(0, F_GETFL);
    // 2.修改文件描述符的属性
    //0000 1000 1111 1111 1111
    //0010 0000 0000 0000 0000
    //0010 1000 1111 1111 1111
    //置位:置1:|
    //复位:置0:&
    flags |= O_NONBLOCK;//非阻塞
    // 3.设置文件描述符的属性
    fcntl(0,F_SETFL,flags);

    char buf[128] = {0};
    while (1)
    {
        if (fgets(buf, sizeof(buf), stdin) == NULL)
        {
            printf("err\n");
        }
        else
            printf("buf:%s\n", buf);
        sleep(1);
    }
    return 0;
}

  1. 信号驱动IO/异步IO:异步通知方式,需要底层驱动的支持(了解)

异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。

1. 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。

2. 应用程序收到信号后做异步处理即可。

应用程序需要把自己的进程号告诉内核,并打开异步通知机制。

//1.设置将文件描述符和进程号提交给内核驱动
//一旦fd有事件响应, 则内核驱动会给进程号发送一个SIGIO的信号
   fcntl(fd,F_SETOWN,getpid());

//2.设置异步通知
    int flags;
    flags = fcntl(fd, F_GETFL); //获取原属性
    flags |= O_ASYNC;       //给flags设置异步   O_ASUNC 通知
    fcntl(fd, F_SETFL, flags);  //修改的属性设置进去,此时fd属于异步
    
//3.signal捕捉SIGIO信号 --- SIGIO:内核通知会进程有新的IO信号可用
//一旦内核给进程发送sigio信号,则执行handler
    signal(SIGIO,handler);

阻塞IO(Blocking IO)

非阻塞IO(Non-blocking IO)

信号驱动IO(Signal-driven IO)

同步性

同步

非同步

异步

描述

调用IO操作的线程会被阻塞,直到操作完成

调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作

当IO操作可以进行时,内核会发送信号通知进程

特点

最常见、效率低、不耗费cpu,

轮询、耗费CPU,可以处理多路IO,效率高

异步通知方式,需要底层驱动的支持

适应场景

小规模IO操作,对性能要求不高

高并发网络服务器,减少线程阻塞时间

实时性要求高的应用,避免轮询开销

【0】复习

四次挥手:FIN ACK FIN ACK

UDP:

IO模型:阻塞、非阻塞、异步、IO多路复用

阻塞:效率低、常见、不耗费CPU

非阻塞:耗费CPU、可以处理多路IO,效率相对高,轮询

异步:底层驱动的支持

TCP:粘包、拆包

UDP:丢包

【1】Linux IO模型

场景假设二

假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?

  1. 每个房间不停的看孩子醒没醒:可行,但是超级无敌累
  2. 听孩子哭不哭:不可行,只知道有孩子醒了,但是不知道是哪一个
  3. 一直在一个房间呆着:看不到另外两个孩子
  4. 妈妈在客厅睡觉,孩子醒了自己走出来告诉妈妈:不累可以得到休息并且可以及时知道孩子醒了

  • 应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;

● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;

● 比较好的方法是使用I/O多路复用技术。其(select)基本思想是:

○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。

○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

○ 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

IO多路复用:select poll epoll

select

  1. 特点
  1. 一个进程最多可以监听1024个文件描述符
  2. select每次被唤醒之后,要重新轮询表,效率低
  3. select每次都会清空未发生响应的文件描述符,每次都要经过用户空间拷贝内核空间,效率低,开销大
  1. 编程步骤
  1. 构造一张关于文件描述符的表
  2. 清空表 FD_ZERO
  3. 将关心的文件描述符添加到表中 FD_SET
  4. 调用select函数,监听 select
  5. 判断到底是哪一个或者哪些文件描述符发生了事件 FD_ISSET
  6. 做对应的逻辑处理
  1. 函数接口

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
功能:
	实现IO的多路复用
参数:
	nfds:关注的最大的文件描述符+1
    readfds:关注的读表
	writefds:关注的写表 
	exceptfds:关注的异常表
	timeout:超时的设置
		NULL:一直阻塞,直到有文件描述符就绪或出错
		时间值为0:仅仅检测文件描述符集的状态,然后立即返回
		时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值

struct timeval {
    long tv_sec;		/* 秒 */
    long tv_usec;	/* 微秒 = 10^-6秒 */
};

返回值:
	成功:准备好的文件描述符的个数
	失败:-1 
	0:超时检测时间到并且没有文件描述符准备好	
注意:
	select返回后,关注列表中只存在准备好的文件描述符
操作表:
void FD_CLR(int fd, fd_set *set); //清除集合中的fd位
void FD_SET(int fd, fd_set *set);//将fd放入关注列表中
int  FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中  是--》1   不是---》0
void FD_ZERO(fd_set *set);//清空关注列表

  1. 练习

练习一:输入鼠标的时候, 响应鼠标事件, 输入键盘的时候, 响应键盘事件 (两路IO)

#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    int sel;
    char buf[128] = {0};
    // 输入鼠标的时候, 响应鼠标事件, 输入键盘的时候, 响应键盘事件
    int fd = open("/dev/input/mouse1", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("open mouse okk\n");
    //  1.构造一张关于文件描述符的表
    fd_set rfds;
    while (1)
    {
        //  2.清空表 FD_ZERO
        FD_ZERO(&rfds);
        // 3.将关心的文件描述符添加到表中 FD_SET
        FD_SET(fd, &rfds); // 鼠标
        FD_SET(0, &rfds);  // 键盘
        // 4.调用select函数,监听  select
        sel = select(fd + 1, &rfds, NULL, NULL, NULL);
        if (sel < 0)
        {
            perror("select err");
            return -1;
        }
        printf("有%d件事件发生\n", sel);
        // 5.判断到底是哪一个或者哪些文件描述符发生了事件 FD_ISSET
        if (FD_ISSET(0, &rfds))
        {
            // 6.做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard:%s\n", buf);
        }
        if (FD_ISSET(fd, &rfds))
        {
            // 6.做对应的逻辑处理
            read(fd, buf, sizeof(buf));
            printf("mouse:%s\n", buf);
        }
    }
    close(fd);

    return 0;
}

练习二:用select创建并发服务器,可以同时连接多个客户端 (0,sockfd)

循环服务器:一个服务器可以连接多个客户端,但是不能同时

并发服务器:一个服务器可以同时连接多个客户端并处理请求

#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    int sel, acceptfd, len;
    char buf[128] = {0};
    int max=0;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    // saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    saddr.sin_addr.s_addr = INADDR_ANY;
    len = sizeof(caddr);
    // uint32_t
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 监听
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");
    //  1.构造一张关于文件描述符的表
    fd_set rfds,tempfds;

    //  2.清空表 FD_ZERO
    FD_ZERO(&rfds);
    FD_ZERO(&tempfds);
    // 3.将关心的文件描述符添加到表中 FD_SET
    FD_SET(sockfd, &rfds); // sockfd
    FD_SET(0, &rfds);      // 键盘
    max=sockfd;
    while (1)
    {
        tempfds=rfds;
        // 4.调用select函数,监听  select
        sel = select(max + 1, &tempfds, NULL, NULL, NULL);
        if (sel < 0)
        {
            perror("select err");
            return -1;
        }
        printf("有%d件事件发生\n", sel);
        // 5.判断到底是哪一个或者哪些文件描述符发生了事件 FD_ISSET
        if (FD_ISSET(0, &tempfds))
        {
            // 6.做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard:%s\n", buf);
        }
        if (FD_ISSET(sockfd, &tempfds))
        {
            // 6.做对应的逻辑处理
            acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
            if (acceptfd < 0)
            {
                perror("accept err");
                return -1;
            }
            printf("acceptfd:%d\n", acceptfd);
            printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
        }
    }
    close(sockfd);

    return 0;
}

练习三:用select创建并发服务器,可以与多个客户端进行通信(监听键盘、socket、多个acceptfd)

#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    int sel, acceptfd, len, ret;
    char buf[128] = {0};
    int max = 0;
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定信息
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    // saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    saddr.sin_addr.s_addr = INADDR_ANY;
    len = sizeof(caddr);
    // uint32_t
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 监听
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");
    //  1.构造一张关于文件描述符的表
    fd_set rfds, tempfds;

    //  2.清空表 FD_ZERO
    FD_ZERO(&rfds);
    FD_ZERO(&tempfds);
    // 3.将关心的文件描述符添加到表中 FD_SET
    FD_SET(sockfd, &rfds); // sockfd
    FD_SET(0, &rfds);      // 键盘
    max = sockfd;
    while (1)
    {
        tempfds = rfds;
        // 4.调用select函数,监听  select
        sel = select(max + 1, &tempfds, NULL, NULL, NULL);
        if (sel < 0)
        {
            perror("select err");
            return -1;
        }
        printf("有%d件事件发生\n", sel);
        // 5.判断到底是哪一个或者哪些文件描述符发生了事件 FD_ISSET
        if (FD_ISSET(0, &tempfds))
        {
            // 6.做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard:%s\n", buf);
        }
        if (FD_ISSET(sockfd, &tempfds))
        {
            // 6.做对应的逻辑处理
            acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
            if (acceptfd < 0)
            {
                perror("accept err");
                return -1;
            }
            printf("acceptfd:%d\n", acceptfd);
            printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
            // 将用于通信的文件描述符放到表中
            FD_SET(acceptfd, &rfds);
            if (acceptfd > max)
                max = acceptfd;
        }
        for (int i = sockfd + 1; i <= max; i++)
        {
            if (FD_ISSET(i, &tempfds))/*  */
            {
                ret = recv(i, buf, sizeof(buf), 0);
                if (ret < 0)
                {
                    perror("recv err");
                    return -1;
                }
                else if (ret == 0)
                {
                    printf("client exit\n");
                    // 关闭文件描述符
                    close(i);
                    FD_CLR(i, &rfds); // 从原表中删除
                    while (!FD_ISSET(max, &rfds))
                        max--;
                }
                else
                {
                    printf("buf:%s\n", buf);
                    memset(buf, 0, sizeof(buf));
                }
            }
        }
    }
    close(sockfd);

    return 0;
}

  1. 超时检测
概念

什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理

比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;

必要性

1. 避免进程在没有数据时无限制的阻塞;

  1. 规定时间未完成语句应有的功能,则会执行相关功能;

poll

  1. 特点
  1. 优化了文件描述符的限制
  2. poll每次唤醒之后,需要重新轮询,效率低,耗费CPU
  3. poll不需要构造文件描述符的表,采用结构体数组,每次调用也要经过用户空间到内核空间的拷贝
  1. 编程步骤
  1. 创建结构体数组
  2. 将关心的文件描述符添加到数组中,并赋予事件
  3. 保存数组内最后一个有效元素下标
  4. 调用poll函数,监听
  5. 判断结构体内文件描述符触发的事件
  6. 根据不同文件描述符触发的不同事件做对应的逻辑处理

例:

  1. 函数接口

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能: 监视并等待多个文件描述符的属性变化
参数:
	  1.struct pollfd *fds:   关心的文件描述符数组,大小自己定义
   若想检测的文件描述符较多,则建 立结构体数组struct pollfd fds[N]; 
           struct pollfd{
	                  int fd;	 //文件描述符
	             short events;//等待的事件触发条件----POLLIN读时间触发
	             short revents;	//实际发生的事件(未产生事件: 0 ))
                            }
	    2.   nfds:    最大文件描述符个数
	    3.  timeout: 超时检测 (毫秒级):1000 == 1s      
                    如果-1,阻塞          如果0,不阻塞
返回值:  <0 出错		>0 表示有事件产生;
              如果设置了超时检测时间:&tv	   ==0 表示超时时间已到;

标签:addr,int,编程,网络,笔记,sockfd,include,buf,客户端
From: https://blog.csdn.net/2403_88313638/article/details/144618395

相关文章

  • C# 异步编程模型【代码之美系列】
    ......
  • 弹性波动力学笔记(五) 应变张量简介下
    2.5ProofthatstraintensorisatensorToprovethat\(\varepsilon_{ij}\)isatensoritisnecessarytoshowthatunderarotationofaxisthecomponentstransformaccordingto\[\varepsilon_{kl}^{'}=a_{ki}a_{lj}\varepsilon_{ij}\tag{2.5.1}......
  • day1——伙伴匹配学习笔记
    了解了一些Java8特性lambda语法(parameters)->expression(parameters)->{statements;}相当于是函数的另一种写法,更优雅。stream()用法以及和parallelStream()的一些对比详见博客java8新特性-流-stream()和parallelStream()求求你们了,别再乱用parallelStream......
  • 《程序员修炼之道:从小工到专家》读书笔记——9
    生活平衡与职业规划程序员的职业生涯不仅仅是技术的不断突破,也需要有良好的生活平衡。长时间的高强度工作可能会导致疲劳、压力过大,甚至影响到身体健康。因此,专家程序员往往会更加注重工作与生活的平衡,保持身心健康,避免过度疲劳。在生活平衡方面,专家程序员通常会设定清晰的工作......
  • Go 高级编程 三 内存模型
    内存模型并发编程模型:多线程、消息传递。从理论上来说,多线程和基于消息的并发编程是等价的。go语言是基于消息并发模块的编程语言,将基于CSP模型的并发编程内置到语言中,可以通过go关键字来启动一个goroutinego的goroutiune之间是共享内存的。Goroutinegoroutiune是一种轻量级......
  • 《程序员修炼之道:从小工到专家》读书笔记——7
    追求卓越:技术深耕与广度扩展专家程序员不仅仅满足于掌握当前的技术栈,他们会不断深耕自己的技术领域,成为该领域的权威。与此同时,他们也会扩展自己的技术广度,了解多种技术,保持技术的多样性。无论是专注于某一领域,还是跨领域发展,专家程序员都要追求技术的卓越。在技术深耕方面,专家......
  • 《程序员修炼之道:从小工到专家》读书笔记——8
    领导力与影响力:从程序员到技术领导者程序员的职业发展不仅仅是技术水平的提高,很多时候,技术人员最终会走向管理岗位,成为技术团队的领导者。无论是在团队内部担任技术负责人,还是在公司中担任更高层次的技术管理岗位,领导力和影响力是专家程序员所必备的素质。作为技术领导者,专家程......
  • 《代码大全》阅读笔记#2
    书中提到的一项关键概念就是“重构”。重构不仅仅是对现有代码进行小幅度的修改,而是一个持续的过程。它包括了改善代码的结构、提高代码的可读性、减少冗余、消除重复等,使得代码在不断变化的需求面前能够灵活适应。“模块化”是《代码大全》中反复提到的另一个重要理念。通过将代......
  • 8086汇编(16位汇编)学习笔记04.乘除和移位指令
    8086汇编(16位汇编)学习笔记04.乘除和移位指令-C/C++基础-断点社区-专业的老牌游戏安全技术交流社区-BpSend.net乘法和除法指令用的不多,因为效率很低比较指令CMP(compare)•格式:CMPOPD,OPS•功能:(OPD)—(OPS),跟减法指令很像,但是不存结果•说明:目的操作数减去源操作数......
  • 新手编程实践,问题求解
    importtkinterastkimportrandomfromtkinter.constantsimportDISABLEDfromtkinterimportmessageboxclassDbutton(tk.Button):def__init__(self,master=None,**kwargs):self.processed=False#表明按钮是否已经被按过了self.coun......