网络编程学习笔记

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. 创建流式套接字(SOCK_STREAM)(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 成功接收的字节个数





​​​​​​​

3.2.服务器

#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;
}

3.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>

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. 自动获取本机地址0.0.0.0

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

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

4.网络模型

4.1.网络的体系结构

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

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

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

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

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

OSI模型是最理想的模型

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

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

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

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

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

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

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

4.3.TCP/IP模型---规范

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

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

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

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

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

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

4.4.常见协议

网络接口和物理层:

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:邮件传输协议

4.4.1.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黑客技术

4.4.2.网络调试命令

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

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

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

根据域名得到服务器IP;

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

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

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

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

4.4.2.1.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

4.4.2.3.手机调试工具

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

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

4.5.TCP/UDP

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

TCP:全双工通信、面向连接、可靠,面相字节流

UDP:面向数据报,传输效率高,面向无连接,数据传输不可靠

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

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

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

适用场景

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

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

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

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

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

适用场景

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

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

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

4.6wireshark抓包工具

  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.7三次握手与四次挥手

4.7.1.三次握手

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

在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是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据

4.7.2.四次挥手

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

TCP连接终止需四个分节。

类比挂电话的过程:

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

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

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

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

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

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

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

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

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

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

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

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

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

总结

TCP 断开连接时需要四次挥手的主要原因是 TCP 协议的半关闭特性,使得服务器在收到客户端的关闭请求后,不能立即关闭连接,而是需要先确认客户端的请求,等自己也没有数据要发送时再发送 FIN 包请求关闭连接,因此不能像建立连接时那样将两个操作合并,需要四次交互才能完成连接断开。

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

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

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

4.8.UDP

4.8.1.通信流程

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

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

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

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

4.8.2.函数接口

4.8.2.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:客户端退出

4.8.2.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

4.8.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;
}

4.8.4.客户端

#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

​​​​​​​

5.Linux IO模型

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

5.1.阻塞式IO

阻塞式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有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。

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

5.2. 非阻塞IO

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

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

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

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

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

5.2.1.设置非阻塞的方式

    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;
}

5.3信号驱动IO/异步IO

信号驱动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操作,对性能要求不高

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

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

5.4.IO多路复用:select poll epoll

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

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

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

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

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

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

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

5.4.1. select

5.4.1.1.特点
  1. 一个进程最多可以监听1024个文件描述符
  2. select每次被唤醒之后,要重新轮询表,效率低
  3. select每次都会清空未发生响应的文件描述符,每次都要经过用户空间拷贝内核空间,效率低,开销大
  4. ​​​​​​​select 可以跨平台

5.4.1.2.编程步骤
  1. 构造一张关于文件描述符的表
  2. 清空表 FD_ZERO
  3. 将关心的文件描述符添加到表中 FD_SET
  4. 调用select函数,监听 select
  5. 判断到底是哪一个或者哪些文件描述符发生了事件 FD_ISSET
  6. 做对应的逻辑处理

5.4.1.3.函数接口

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);//清空关注列表

5.4.1.4.程序
#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;
}

5.4.2.poll

5.4.2.1.特点
  1. 优化了文件描述符的限制
  2. poll每次唤醒之后,需要重新轮询,效率低,耗费CPU
  3. poll不需要构造文件描述符的表,采用结构体数组,每次调用也要经过用户空间到内核空间的拷贝

5.4.2.2.编程步骤
  1. 创建结构体数组
  2. 将关心的文件描述符添加到数组中并赋予事件
  3. 保存数组内最后一个有效元素
  4. 调用poll函数监听
  5. 判断结构体内文件描述符触发的事件
  6. 根据不同文件描述符触发的不同事件做对应的逻辑处理

例:

5.4.2.3.函数接口

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 表示超时时间已到;

5.4.3.IO多路复用:epoll

epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;

eg:1GB机器上,这个上限10万个左右。

每个fd上面有callback(回调函数)函数,只有产生事件的fd才有主动调用callback,不需要轮询。

注意:

Epoll处理高并发,百万级

1. 红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)

2. 就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读事件还是写事件)。

5.4.3.1.特点
  1. 监听的文件描述符没有了限制
  2. 异步IO,epoll当有事件唤醒之后,发生事件的文件描述符会主动的调用callback回调函数,拿到对应的文件描述符。不需要要轮询,效率高
  3. epoll不需要构造表,只需要从用户空间拷贝到内核空间一次

​​​​​​​​​​​​​​5.4.3.2.编程步骤
  1. 创建红黑树和就绪链表 epoll_create
  2. 将关心的文件描述符和事件上树 epoll_ctl
  3. 阻塞等待事件发生,一旦产生事件,则进行处理 epoll_wait
  4. 根据准备好的文件描述符做对应的逻辑处理

​​​​​​​​​​​​​​5.4.3.3.函数接口

int epoll_create(int size); 
功能:创建红黑树根节点(创建epoll实例) , 同时也会创建就绪链表
返回值:成功时返回一个实例epfd(二叉树句柄),失败时返回-1。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll属性,比如给红黑树添加节点
参数: 1. epfd:   epoll_create函数的返回句柄。//一个标识符
     2. op:表示动作类型,有三个宏:			        
                EPOLL_CTL_ADD:注册新的fd到epfd中
			      EPOLL_CTL_MOD:修改已注册fd的监听事件
			      EPOLL_CTL_DEL:从epfd中删除一个fd
     3. 要操作的文件描述符
     4. 结构体信息: 
 typedef union epoll_data {
        
        
               int fd;      //要添加的文件描述符
               uint32_t u32;  typedef unsigned int
               uint64_t u64;   typedef unsigned long int
        } epoll_data_t;

   struct epoll_event {
        
        
       uint32_t events; 事件
       epoll_data_t data; //共用体(看上面)
		};

	  关于events事件:
			 EPOLLIN:  表示对应文件描述符可读
		    EPOLLOUT: 可写
			 EPOLLPRI:有紧急数据可读;
		    EPOLLERR:错误;
		    EPOLLHUP:被挂断;
			 EPOLLET:触发方式,边缘触发;(默认使用边缘触发)
			 ET模式:表示状态的变化;
           NULL: 删除一个文件描述符使用,无事件
           
返回值:成功:0, 失败:-1

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件产生
   内核会查找红黑树中有事件响应的文件描述符, 并将这些文件描述符放入就绪链表
    就绪链表中的内容, 执行epoll_wait会同时复制到第二个参数events

参数: 	epfd:句柄;
		events:用来保存从就绪链表中响应事件的集合;
		maxevents:  表示每次在链表中拿取响应事件的个数;
		timeout:超时时间,毫秒,0立即返回  ,-1阻塞	

返回值: 成功: 实际从链表中拿出的文件描述符数目     失败时返回-1

 5.4.4.超时检测

概念

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

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

必要性

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

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

  1. 总结

select

poll

epoll

监听个数

一个进程最多监听1024个文件描述符

由程序员自己决定

百万级

方式

每次都会被唤醒,都需要重新轮询

每次都会被唤醒,都需要重新轮询

红黑树内callback自动回调,不需要轮询

效率

文件描述符数目越多,轮询越多,效率越低

文件描述符数目越多,轮询越多,效率越低

不轮询,效率高

原理

每次使用select后,都会清空表

每次调用select,都需要拷贝用户空间的表到内核空间

内核空间负责轮询监视表内的文件描述符,当事件发生后,把发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环

不会清空结构体数组

每次调用poll,都需要拷贝用户空间的结构体到内核空间

内核空间负责轮询监视结构体数组内的文件描述符,当事件发生后,把发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环

不会清空表

epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时)

通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝

特点

一个进程最多能监听1024个文件描述符

select每次被唤醒,都要重新轮询表,效率低

select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间

优化文件描述符的个数限制

poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu)

poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间

监听的文件描述符没有个数限制(取决于自己的系统)

异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高

epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。

结构

文件描述符表(位表)

结构体数组

红黑树和就绪链表

开发复杂度

6.服务器模型

在网络通信中,通常要求一个服务器连接多个客户端

为了处理多个客户端的请求,通常有多种表现形式

6.1.循环服务器:

循环服务器:一个服务器在同一时间只能处理一个客户端的请求

socket()(;
bind();
listen();
while(1)
    accept();
        while(1)
           recv()/send();
 close(); 

6.2.并发服务器

并发服务器:一个服务器在同一时间段可以处理多个客户端的请求

6.2.1.IO多路复用

select 、poll、 epoll

6.2.2.IO多线程:每有一个客户端连接就创建一线程进行通信

为什么要去创建线程----》通信

什么时间创建线程-----》accept之后pthread_create

子线程:通信

主线程:循环等待下一个客户端连接

#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>
#include <pthread.h>

void *handler(void *arg)
{
        
        
    char buf[128] = {
        
        0};
    int ret = 0;
    int acceptfd=*(int *)arg;
    // 通信
    while (1)
    {
        
        
        ret = recv(acceptfd, buf, sizeof(buf), 0);
        if (ret < 0)
        {
        
        
            perror("recv err");
            pthread_exit(NULL);
        }
        else if (ret == 0)
        {
        
        
            printf("client exit\n");
            break;
        }
        else
        {
        
        
            printf("buf:%s\n", buf);
            memset(buf, 0, sizeof(buf));
        }
    }
    // 关闭套接字
    close(acceptfd);
    pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
        
        
    int acceptfd;
    int len;
    pthread_t tid;
    // 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));
        pthread_create(&tid, NULL, handler,&acceptfd);
        pthread_detach(tid);
    }
    close(sockfd);

    return 0;
}

6.2.3.多进程:每有一个客户端连接就创建一个进程进行通信

为什么要去创建进程----》通信

什么时间创建进程-----》accept之后fork

子进程:通信

父进程:循环等待下一个客户端连接

子进程资源回收:

wait,waitpid?

SIGCHLD:当子进程退出会给父进程发送的信号

#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>
#include <signal.h>
#include <sys/wait.h>

void handler(int sig)
{
        
        
    waitpid(-1,NULL,WNOHANG);
}

int main(int argc, char const *argv[])
{
        
        
    char buf[128] = {0};
    int ret = 0;
    int acceptfd;
    pid_t pid;
    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");
    signal(SIGCHLD, handler);
    // 接收连接
    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));
        pid = fork();
        if (pid < 0)
        {
        
        
            perror("fork err");
            return -1;
        }
        else if (pid == 0)
        {
        
        
            close(sockfd);
            // 通信
            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);
            exit(0);
        }
        else
            close(acceptfd);
    }

    return 0;
}

6.2.4.并发服务器

IO多路复用

优点:节省资源,系统开销小,性能高

缺点:代码的复杂性高

多线程

优点:资源开销小

缺点:多个线程安全性差

多进程

优点:服务器更稳定,资源独立,安全性高

缺点:资源开销大

7.网络协议头分析

7.1.数据的封装与传递过程

思考:

1. 应用层调用send后,是如何把数据发送到另一台机器的某个进程的。

2. 接收的设备收到数据包后,如何处理给应用层?

思考:在协议栈封装的过程中,这些头部信息具体有什么呢?

7.2.以太网帧完整帧

对于网络层最大数据帧长度是1500字节

对于链路层最大数据长度是1518字节(1500+14+CRC)

发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递

7.3.以太网头部

7.4.IP头

IHL:数据流控控制

7.5.TCP头

Src: 源

Dst:目标

Seq:序列号

Ack:应答号(应答包的应答号)

数据包:

A:ACK:应答包

S:SYN:握手包(同步包),连接时产生

P:PSH:(PUSH)数据包,传输数据产生

F:FIN:挥手包,断开连接时产生

7.6.UDP头

length:固定udp数据包的长度(确保udp在传输的过程中不会粘包)

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

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

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

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

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

为什么UDP会造成丢包:

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

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

8.setsockopt:设置套接字属性

set:设置 sock:套接字 option:属性

#include<sys.time.h>
int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen)
功能:获得/设置套接字属性
参数:
    sockfd:套接字描述符
    level:协议层
    optname:选项名
    optval:选项值
    optlen:选项值大小
返回值:     成功 0                  失败-1

8.1.socket属性

(int 类型中 允许则为1或其他值 , 不允许则为0)

选项名称

说明

数据类型

========== SOL_SOCKET 应用层 ===========

SO_BROADCAST

允许发送广播数据

int

SO_DEBUG

允许调试

int

SO_DONTROUTE

不查找路由

int

SO_ERROR

获得套接字错误

int

SO_KEEPALIVE

保持连接

int

SO_LINGER

延迟关闭连接

struct linger

SO_OOBINLINE

带外数据放入正常数据流

int

SO_RCVBUF

接收缓冲区大小

int

SO_SNDBUF

发送缓冲区大小

int

SO_RCVLOWAT

接收缓冲区下限

int

SO_SNDLOWAT

发送缓冲区下限

int

SO_RCVTIMEO

接收超时

struct timeval

SO_SNDTIMEO

发送超时

struct timeval

SO_REUSEADDR

允许重用本地地址和端口

int

SO_TYPE

获得套接字类型

int

SO_BSDCOMPAT

与BSD系统兼容

int

========== IPPROTO_IP IP层/网络层 =============

IP_HDRINCL

在数据包中包含IP首部

int

IP_OPTINOS

IP首部选项

int

IP_TOS

服务类型

int

IP_TTL

生存时间

int

IP_ADD_MEMBERSHIP

将指定的IP加入多播组

struct ip_mreq

========== IPPRO_TCP 传输层 ============

TCP_MAXSEG

TCP最大数据段的大小

int

TCP_NODELAY

不使用Nagle算法

int

8.3.超时检测

8.3.1.超时检测的设置方法

8.3.1.1.通过函数自带参数进行设置

select poll

8.3.1.2.通过设置套接字属性进行设置

  1. 3. alarm函数与sigaction函数结合

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
功能:对接收到的指定信号处理
参数:signum:要捕获的信号
	act:接收到信号之后对信号进行处理的结构体
	oldact:接收到信号之后,保存原来对此信号处理的各种方式与信号(可用来做备份)。如果不需要备份,
    此处可以填NULL
struct sigaction 
{
         
         
    void     (*sa_handler)(int); //信号处理函数
    void     (*sa_sigaction)(int, siginfo_t *, void *);  //查看信号的各种详细信息
    sigset_t   sa_mask;
    int        sa_flags;      //信号属性; SA_RESTART自重启属性
#define SA_RESTART  0x10000000
    void     (*sa_restorer)(void);//不再使用
   };     
    //设置信号属性
    struct sigaction act;
    sigaction(SIGALRM,NULL,&act);//获取原属性
    act.sa_handler=handler;//修改属性
    sigaction(SIGALRM,&act,NULL);//将修改的属性设置回去
返回值:
	成功:0
	出错:-1,并将errno设置为指示错误

9.广播与组播(broadcast & multicast)

9.1.广播(udp)

broadcast——广播

9.1.1.理论:

前面介绍的数据包发送方式只有一个接受方,称为单播

如果同时发给局域网中的所有主机,称为广播

只有用户数据报(使用UDP协议)套接字才能广播

一般被设计成局域网搜索协议

  • 广播地址:局域网中主机号最大的一个 192.168.50.255

9.1.2.发送者

  1. 创建数据报套(SOCK_DGRAM)接字(socket)
  2. 由于原本的套接字不允许广播,所以要设置套接字广播属性(setsockopt)
  3. 指定网络(接收者)信息
  4. 发送消息   sendto
  5. 关闭套接字 close

#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);
    //设置套接字广播属性
    int optval=1;
    setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&optval,sizeof(optval));

    // 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;
}

9.1.3.接收者

  1. 创建数据报套接字
  2. 指定网络信息
  3. 绑定
  4. 接收消息
  5. 关闭套接字

9.1.4.缺点:

广播方式发给所有的主机,过多的广播会大量的占用网络带宽,造成广播风暴,影响正常的通信

广播风暴: 网络长时间被大量的广播数据包所占用,使正常的点对点通信无法正常进行,其外在表现为网络速度奇慢无比,甚至导致网络瘫痪

9.2.组(多)播

9.2.1.理论

单播方式只能发给一个接收方。

广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。

播是一个人发送,加入到多播组的人接收数据。

多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)

  • D类:224.0.0.0 - 239.255.255.255

9.2.2.发送者

  1. 创建数据报套接字
  2. 指定网络(接收者)信息
  3. 发送消息
  4. 关闭套接字

9.2.2.接收者

  1. 创建数据报套接字
  2. 设置多播属性,将自己的IP加入到多播组中
  3. 指定网络信息
  4. 绑定
  5. 接收消息
  6. 关闭套接字

#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);

    // 设置多播属性
    // ip_mreq;
    struct ip_mreq mreq;
    mreq.imr_multiaddr.s_addr = inet_addr(argv[2]); // 组播IP
    mreq.imr_interface.s_addr = INADDR_ANY;
    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

    // 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;
}

9.3.本地套接字

9.3.1.特性

  1. socket同样可以用于本地间进程通信,创建套接字时使用本地协议AF_LOCAL或AF_UNIX
  2. 分为流式套接字和数据报套接字
  3. 和其他进程间通信相比使用方便、效率更高,常用于前后台进程通信。

9.3.2.流程(tcp)

9.3.2.1.服务端
  1. socket()
  2. struct sockaddr_un
  3. bind()
  4. listen()
  5. acceptfd()
  6. recv()/send()
  7. close()

9.3.2.2.客户端
  1. socket()
  2. struct sockaddr_un
  3. connect()
  4. send/recv()
  5. close()

10.sql数据库

10.1.数据库的概念

数据库是“按照数据结构来组织、存储和管理数据的仓库”。是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。

数据库是存放数据的仓库。它的存储空间很大,可以存放百万条、千万条、上亿条数据。但是数据库并不是随意地将数据进行存放,是有一定的规则的,否则查询的效率会很低。当今世界是一个充满着数据的互联网世界,充斥着大量的数据。即这个互联网世界就是数据世界。数据的来源有很多,比如出行记录、消费记录、浏览的网页、发送的消息等等。

10.2.常用的数据库

大型数据库 :Oracle

中型数据库 :Server是微软开发的数据库产品,主要支持windows平台

小型数据库 : MySQL是一个小型关系型数据库管理系统。开放源码 (嵌入式不需要存储太多数据)

10.2.1.MySQLSQLite区别:

MySQL和SQLite是两种不同的数据库管理系统,它们在多个方面有所不同。

1. 性能和规模:MySQL通常用于大型应用程序和网站,它可以处理大量数据和高并发访问。SQLite则更适合于小型应用程序或移动设备,因为它是一个轻量级的数据库引擎,不需要独立的服务器进程,可以直接访问本地文件。

2. 部署和配置:MySQL需要单独的服务器进程来运行,需要配置和管理数据库服务器。而SQLite是一个嵌入式数据库,可以直接嵌入到应用程序中,不需要单独的服务器进程。

3. 功能和特性:MySQL提供了更多的功能和高级特性,比如存储过程、触发器、复制和集群支持等。SQLite则是一个轻量级的数据库引擎,功能相对较少,但对于简单的数据存储和检索已经足够。

4. 跨平台支持:SQLite在各种操作系统上都能够运行,而MySQL需要在特定的操作系统上安装和配置数据库服务器。

总之,MySQL适用于大型应用程序和网站,需要处理大量数据和高并发访问,而SQLite适用于小型应用程序或移动设备,对性能和规模要求没有那么高。

10.3.SQL基础

SQLite的源代码是C,其源代码完全开放。它是一个轻量级的嵌入式数据库。

SQLite有以下特性:

零配置一无需安装和管理配置;

储存在单一磁盘文件中的一个完整的数据库;

数据库文件可以在不同字节顺序的机器间自由共享;

支持数据库大小至2TB(1024G = 1TB);//嵌入式足够

足够小,全部源码大致3万行c代码,250KB;

比目前流行的大多数数据库对数据的操作要快;

创建:

手动:

使用sqlite3工具,手工输入命令

命令行输入

代码:

利用代码编程,调用接口

  1. 在虚拟机中安装sqlite3

1.安装服务

在终端输入 sudo apt-get install libreadline-dev

  1. 安装数据库

tar xf sqlite-autoconf-3460000.tar.gz

cd sqlite-autoconf-3460000

./configure

make

sudo make install

  1. 安装完成测试是否安装成功

sqlite3 -version

  1. 图形化工具的安装

sudo apt-get install sqlitebrowser

10.4.SQL语句的使用

SQL基本语句的基本使用

10.4.1.命令的方式操作

格式:sqlite3 数据库文件名(stu.db)

(创建一个新的数据库)

两种命令:

10.4.1.1. sqlite3系统命令

(类似Windows系统命令,开机关机等,都是以.开头的)

都是以 '.' 开头的

a. .help 查看所有支持的命令

b. .quit 退出

c. .tables 查看有哪些表

d. .schema stu2 查看表结构

10.4.2. SQL命令

(具体对数据库怎样操作,对数据库增删改查用SQL命令)

SQL命令是以 “;” 结尾

在库当中创建一个表

(在数据库里面不严格检查数据类型,char可以表示字符,也可以表示字符串 )

10.4.2. 1.创建一个表

create table stu(id int,name char,score float);

create table stu1(id int primary key, name char, score float);

注:把id字段设置为主键(在表中唯一)

字符串:char string text

小数:float real

不支持严格的类型检查的;

10.4.2. 2. 删除一个表

drop table <table_name>

...>;

10.4.2. 3. 向表里面插入数据

insert into <table_name> values(value1, value2,…);

insert into stu values(1,'xiaomingx',99.9);

//只插入部分字段 id name score

insert into stu(id,name) values(4,'xiaoming')

10.4.2.4. 查找数据
10.4.2.4. 1.查询表中所有记录

select * from <table_name>;

(*表示查询所有的值)

10.4.2.4. 2.指定条件查询表中记录

select * from <table_name> where <expression>;

select * from stu where id=2;

select * from stu where id=2 and name='lisi';

select * from stu where id=1 or name='zhangsan';

select score from stu where name='LiSi' or id=3; //满足条件的某列

select name,score from stu where name='LiSi' or id=3;

select * from stu limit 5; //只查询前n条记录

select * from stu order by id desc; //按id从大到小进行排序

10.4.2.3.修改(更新)数据

update <table_name> set <f1=value1>, <f2=value2>… where <expression>;

update stu set id=10 where id=1;

10.4.2.4. 增加字段

alter table <table> add column <field> <type> default …;

alter table stu add column class int default 1;(表示添加了一列class,默认值为1)

10.4.2.5. 删除字段

(在数据库当中其实不支持直接删除一个字段(及一列),如果就想删除一列,那么需要三步骤)

1)创建一个student表,从stu表当中复制id,name,score

2) drop table stu;

删除原有的stu表

3) alter table student rename to stu;

重命名

最后一列为1的被删除掉了。

10.4.2.6.删除一行

操作完以后可以图形化界面修改东西,然后在命令行去查看的时候就被修改了。

或者

为什么不用图形化界面而是使用命令方式操作:

因为嵌入式里面用C写代码,C代码里面想实现对数据库进行操作,

用的就上面的命令,而C里面你不能在里面嵌套图像化界面。

sqlite3编程

官方文档:List Of SQLite Functions

头文件:#include <sqlite3.h>

编译:gcc sqlite1.c -lsqlite3

【1】SQL数据库

sqlite3编程

官方文档:List Of SQLite Functions

头文件:#include <sqlite3.h>

编译:gcc sqlite1.c -lsqlite3

函数接口

  1. 打开数据库

int sqlite3_open(char  *path, sqlite3 **db);
功能:打开sqlite数据库,如果数据库不存在则创建它
参数:path: 数据库文件路径
     db: 指向sqlite句柄的指针
返回值:成功返回SQLITE_OK(0),失败返回错误码(非零值)

  1. 返回错误信息

char  *sqlite3_errmsg(sqlite3 *db);
功能:  获取错误信息
返回值:返回错误信息

使用:   fprintf(stderr,"sqlite3_open failed %s\n",sqlite3_errmsg(db));

  1. 关闭数据库

int sqlite3_close(sqlite3 *db);
功能:关闭sqlite数据库
返回值:成功返回SQLITE_OK,失败返回错误码

  1. 执行sql语句接口

int sqlite3_exec(
  sqlite3 *db,                   /* An open database */
  const char *sql,               /* SQL to be evaluated */
  int (*callback)(void*,int,char**,char**),  /* Callback function */
  void *arg,                      /* 1st argument to callback */
  char **errmsg                /* Error msg written here */
);

功能:执行SQL操作
参数:db:数据库句柄
    sql:要执行SQL语句
    callback:回调函数(满足一次条件,调用一次函数,用于查询)
        再调用查询sql语句的时候使用回调函数打印查询到的数据
    arg:传递给回调函数的参数
    errmsg:错误信息指针的地址
返回值:成功返回SQLITE_OK,失败返回错误码
回调函数:
typedef int (*sqlite3_callback)(void *para, int f_num, 
         char **f_value, char **f_name);

功能:select:每找到一条记录自动执行一次回调函数
参数:para:传递给回调函数的参数(由 sqlite3_exec() 的第四个参数传递而来)
    f_num:记录中包含的字段数目
    f_value:包含每个字段值的指针数组(列值)
    f_name:包含每个字段名称的指针数组(列名)
返回值:成功返回SQLITE_OK,失败返回-1,每次回调必须返回0后才能继续下次回调

不使用回调函数执行SQL语句(只用于查询)
int sqlite3_get_table(sqlite3 *db, const  char  *sql, 
   char ***resultp,  int *nrow,  int *ncolumn, char **errmsg);

功能:执行SQL操作
参数:db:数据库句柄
    sql:SQL语句
    resultp:用来指向sql执行结果的指针
    nrow:满足条件的记录的数目(但是不包含字段名(表头 id name score))
    ncolumn:每条记录包含的字段数目
    errmsg:错误信息指针的地址
返回值:成功返回SQLITE_OK,失败返回错误码

#include <stdio.h>
#include <sqlite3.h>

int callback(void *buf, int num, char **value, char **name);

int main(int argc, char const *argv[])
{
           
           
    sqlite3 *db;
    // 1.打开数据库
    if (sqlite3_open("./stu.db", &db) != SQLITE_OK)
    {
           
           
        fprintf(stderr, "open sqlite err :%s\n", sqlite3_errmsg(db));
        return -1;
    }
    printf("open database okk\n");

    // 创建表
    char *errmsg = NULL;
    if (sqlite3_exec(db, "create table if not exists stu(id int,name char, 
​​​​​​​score float)",
                     NULL, NULL, &errmsg) != SQLITE_OK)
    {
           
           
        fprintf(stderr, "create table err :%s\n", errmsg);
        return -1;
    }
    printf("create table okk\n");

    // 插入数据
    // if (sqlite3_exec(db, "insert into stu values(1,'haohao',69.9)",
    //                  NULL, NULL, &errmsg) != SQLITE_OK)
    // {
           
           
    //     fprintf(stderr, "insert err :%s\n", errmsg);
    //     return -1;
    // }
    // printf("insert okk\n");

    printf("请输入要插入的学生人数:");
    int num = 0;
    scanf("%d", &num);
    int id;
    char name[32] = {
           
           0};
    float score;
    char sql[128] = {
           
           0};
//
    for (int i = 0; i < num; i++)
    {
           
           
        printf("请输入学生学号,姓名,成绩:");
        scanf("%d %s %f", &id, name, &score);
        sprintf(sql, "insert into stu values(%d,'%s',%f)", id, name, score);
        if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
        {
           
           
            fprintf(stderr, "insert err :%s\n", errmsg);
            return -1;
        }
        printf("insert okk\n");
    }
    // 查询数据
    if (sqlite3_exec(db, "select * from stu where id =4 ", callback, "HELLO", &errmsg) != SQLITE_OK)
    {
           
           
        fprintf(stderr, "select err :%s\n", errmsg);
        return -1;
    }
    printf("select okk\n");
    // 专门用于查询的函数
    char **result = NULL;
    int row = 0;行
    int column = 0;例
    sqlite3_get_table(db, "select * from stu where id=4", &result, &row, &column, &errmsg);
    printf("row:%d column:%d\n",row,column);
    int k=0;
    for(int i=0;i<=row;i++)
    {
           
           
        for(int j=0;j<column;j++)
            printf("%s ",result[k++]);
        putchar(10);
    }


    // 关闭数据
    sqlite3_close(db);
    return 0;
}

int callback(void *buf, int num, char **value, char **name)
{
           
           
    // 每查询到一条符合条件的数据就会调用一次函数
    static int i = 1;
    printf("%d:%s\n", i++, (char *)buf);
    // num:列数
    // value:值
    // name:列名
    for (int j = 0; j < num; j++)
    {
           
           
        printf("%s ", name[j]);
    }
    putchar(10);
    for (int j = 0; j < num; j++)
    {
           
           
        printf("%s ", value[j]);
    }
    putchar(10);
    printf("**************************************\n");

    return 0; // 一定要return ,不然sqlite3_exec函数会认为查询错误,报错
}

【2】UDP聊天室

服务器

#include "head.h"

link_t *link_creat();
void client_login(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr);
void client_quit(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr);
void client_chat(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr);

int main(int argc, char const *argv[])
{
           
           
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 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 = INADDR_ANY;
    socklen_t len = sizeof(caddr);
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
           
           
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");
    // 创建链表
    link_t *p = link_creat();
    printf("creat ok\n");
    MSG_t msg;
    // 可扩展功能,服务器可以给所有用户发送“公告”
    pid_t pid = fork();
    if (pid < 0)
    {
           
           
        perror("pid err");
        return -1;
    }
    else if (pid == 0)
    {
           
           
        // 可扩展功能,服务器可以给所有用户发送“公告”
        while (1)
        {
           
           
            fgets(msg.text, sizeof(msg.text), stdin);
            if (msg.text[strlen(msg.text) - 1] == '\n')
                msg.text[strlen(msg.text) - 1] = '\0';
            strcpy(msg.name, "server");
            msg.type = chat;
            sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in));
        }
    }
    else
    {
           
           
        while (1)
        {
           
           
            if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len) < 0)
            {
           
           
                perror("recvfrom err");
                return -1;
            }
            printf("%s\n", msg.name);
            switch (msg.type)
            {
           
           

            case login: // 上线函数
                client_login(sockfd, p, msg, caddr);
                break;
            case chat: // 聊天函数
                client_chat(sockfd, p, msg, caddr);
                break;
            case quit: // 下线函数
                client_quit(sockfd, p, msg, caddr);
                break;
            default:
                break;
            }
        }
    }
    close(sockfd);

    return 0;
}
// 创建链表
link_t *link_creat()
{
           
           
    // 开辟空间
    link_t *p = (link_t *)malloc(sizeof(link_t));
    if (p == NULL)
    {
           
           
        printf("malloc err\n");
        return NULL;
    }
    // 初始化
    p->next = NULL;

    return p;
}
// 客户端上线功能:将用户上线消息发送给其他用户,并将用户信息添加到链表中
void client_login(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr)
{
           
           
    printf("%s login.......\n", msg.name);
    while (p->next != NULL)
    {
           
           
        p = p->next;
        sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
    }
    link_t *pnew = (link_t *)malloc(sizeof(link_t));
    if (pnew == NULL)
    {
           
           
        printf("malloc err\n");
        return;
    }
    pnew->next = NULL;
    pnew->addr = caddr;
    p->next = pnew;

    return;
}
// 聊天功能:转发消息到在链表中除了自己的所有用户
void client_chat(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr)
{
           
           
    printf("%s says %s\n", msg.name, msg.text);
    while (p->next != NULL)
    {
           
           
        p = p->next;
        if (memcmp(&(p->addr), &caddr, sizeof(caddr)))
        {
           
           
            sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
    }

    return;
}
// 客户端退出功能:将用户下线消息发送给其他用户,将下线用户从链表中删除
void client_quit(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr)
{
           
           
    printf("%s quit.......\n", msg.name);
    link_t *pdel = NULL;
    while (p->next != NULL)
    {
           
           
        if (memcmp(&(p->next->addr), &caddr, sizeof(caddr)))
        {
           
           
            // 下一个节点不是自己
            p = p->next;
            sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
        else
        {
           
           
            pdel = p->next;
            p->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }
    }
    return;
}

客户端

#include "head.h"

int main(int argc, char const *argv[])
{
           
           
    int ret = 0;
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 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[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t len = sizeof(caddr);

    MSG_t msg;
    // 给服务器发送上线消息
    printf("请输入用户名:");
    fgets(msg.name, sizeof(msg.name), stdin);
    if (msg.name[strlen(msg.name) - 1] == '\n')
        msg.name[strlen(msg.name) - 1] = '\0';
    msg.type = login;
    sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in));

    pid_t pid = fork();
    if (pid < 0)
    {
           
           
        perror("fork err");
        return -1;
    }
    else if (pid == 0) // 循环接受服务器消息
    {
           
           
        while (1)
        {
           
           
            ret = recvfrom(sockfd, &msg, sizeof(MSG_t), 0, NULL, NULL);
            if (ret < 0)
            {
           
           
                perror("recv from err");
                exit(0);
            }
            if (msg.type == login)
                printf("%s login......\n", msg.name);
            else if (msg.type == chat)
                printf("%s says %s\n", msg.name, msg.text);
            else if (msg.type == quit)
                printf("%s quit\n", msg.name);
        }
    }
    else // 循环发送消息
    {
           
           
        while (1)
        {
           
           
            fgets(msg.text, sizeof(msg.text), stdin);
            if (msg.text[strlen(msg.text) - 1] == '\n')
                msg.text[strlen(msg.text) - 1] = '\0';
            if (strcmp(msg.text, "quit"))
            {
           
           
                msg.type = chat;
                sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in));
            }
            else
            {
           
           
                msg.type = quit;
                sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr_in));
                break;
            }
        }
        kill(pid, SIGINT);
        wait(NULL);
    }
    close(sockfd);
    return 0;
}

头文件

#ifndef __HEAD_H__
#define __HEAD_H__

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

//链表节点结构体:
typedef struct node
{
           
           
    struct sockaddr_in addr; //data   memcmp
    struct node *next;
} link_t;

//类型
enum type_t
{
           
           
    login,
    chat,
    quit,
};

//消息对应的结构体(同一个协议)
typedef struct msg_t
{
           
           
    int type;       //'L' C  Q    enum un{login,chat,quit};
    char name[32];  //用户名
    char text[128]; //消息正文
} MSG_t;

#endif

网编项目

云词典项目

云词典项目 · 华清远见工作空间 《云词典项目》

华清速递项目

华清速递项目 · 华清远见工作空间 《华清速递项目》


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值