#网络编程 笔记

认识网络

网络发展史

ARPnetA--->Internet--->移动互联网--->物联网

TCP 

用来检测网络传输中差错的传输控制协议

UDP

用户数据报协议,专门负责对不同网络进行互联的互联网协议

局域网

实现小范围短距离网络通信

广域网

现大范围长距离网络通信

光猫

类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。

路由器

用于连接局域网和外网,进行数据转发。路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN

交换机

用于连接局域网内网,进行数据转发。交换机没有IP分配和IP寻址的功能,所以交换机没有路由器的功能,但大部分路由器都有有交换机的功能。

网线
标准568A:1绿白,2绿,3橙白,4蓝,5蓝白,6橙,7棕白,8棕
标准586B:1橙白,2橙,3绿白,4蓝,5蓝白,6绿,7棕白,8棕
1 2 3 6 上网用 1,2下传 3,6上传 4,5,7电话用 8电源用
直通线:两头都是568B标准,两端线序相同的线,用于不同设备之间互连,比如电脑和路由器。
交叉线:一头是568A,而另一头是568B,两端线序不同,用来同种设备之间互连,比如电脑与电脑。

IP地址

概念

IP是主机在Internet中的标识

与别的机器通信必须具有一个IP地址

IPv4 32位 IPv6 128位

NAT 公网转私网,私网转公网

表示形式:点分十进制,最后转为一个32位的无符号整数

IP地址=网络号+主机号

网络号表示是否在一个网段内

主机号表示在本网段内的ID,同一局域网不能重复

分类

A类 0.0.0.0-127.255.255.255
B类 128.0.0.0-191.255.255.255
C类 192.0.0.0-223.255.255.255
D类 224.0.0.0-239.255.255.255

特殊地址

0.0.0.0 在服务器中,指本机上所有IPv4地址

127.0.0.1 本地回环地址 ,发往改类地址的数据包都被loop back
作用:测试网络接口 访问本地服务 配置调试网络设备

网络地址 每一个网段主机号为0的地址
广播地址 每一个网段主机号最大的地址

全网广播地址 255.255.255.255

子网掩码

长度和IP地址一样,32位整数,作用将某一个IP 划分成网络地址和主机地址;
网络号全为1,主机号全为0
网络号=IP & MASK
主机号=IP & ~MASK

B类地址的子网掩码怎么写?
255.255.0.0

B类地址,同一网段最多可以连接多少个主机?
2^16-2= 65536 - 2 = 65534

已知一个子网掩码号为255.255.255.192,最多可以连接多少台主机?
子网掩码:网络号全为1,主机号全为0
255.255.255.192:255.255.255.1100 0000
26-2=64-2=62

一个IP地址为192.168.3.183 ,计算其网络号与主机号?

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

二级划分 IP=网络号+主机号
三级划分 IP=网络号+子网号+主机号 重新组网,提高资源利用率

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

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

网络模型

网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
网络体系结构指网络的层次结构和每层所使用的协议。
OSI模型

TCP/IP

网络接口和物理层:屏蔽硬件差异,向上层提供统一的操作接口
ppp:拨号协议
ARP:地址解析协议 IP -- MAC
RARP: 反向地址转换协议 MAC-- IP

网络层:IP寻址机器,提供设备到设备的传输
IP(IPv4/IPv6):网间互联协议
ICMP:网络控制管理协议,ping命令使用
IGMP:网络分组管理协议,广播和组播协议

传输层:端口寻址,决定数据交给哪个进程处理(端口号只在网络通信中才有)
TCP: 传输控制协议
UDP:用户数据报协议

应用层:应用协议和应用程序的集合
SMTP/POP3:邮箱传输协议
DNS:域名解析协议
HTTP:超文本传输协议
FTP:文件传输协议
SSH:加密协议
telnet:远程登录协议

TCP

特点 

全双工通信;
面向连接;
可靠:(无丢序)     数据无误,数据无丢失,数据无失序,数据无重复到达

可靠原因
三次握手、四次挥手
序列号和应答机制
超时、错误重传机制
拥塞控制、流量控制(滑动窗口)

适用场景
适合于对传输质量要求高的通信
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能

UDP

特点

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

适用场景
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
适合于广播/组播式通信中
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯

Dos(拒绝式服务)攻击

socket

概念
普通的I/O操作过程 :打开文件->读/写操作->关闭文件
TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作 ----->进行网络通信的两个进程在不同的机器上,如何连接? 网络协议具有多样性,如何进行统一的操作 ?
需要一种通用的网络编程接口:Socket
是一个编程接口
是一种特殊的文件描述符
是一种通信机制
面向连接/无连接

类型
流式套接字(SOCK_STREAM)
数据报套接字(SOCK_DGRAM)
原始套接字(SOCK_RAM)

端口号
为了区分主机接收到的数据包转交给哪个进程处理
TCP/UDP端口号独立
由IANA管理
用两个字节表示

字节序
不同类型的CPU主机,内存存储大于一个字节类型的数据在内存中的存放顺序。
(高地址) 0x12345678 (低地址)
网络字节序 大端序 高地址 0x12 0x34 0x56 0x78 低地址
主机字节序 小端序 低地址 0x78 0x56 0x34 0x12 高地址

网络传输中,需要将每个主机的主机字节序(CPU决定),转换为网络中统一顺序的网络字节序
才能供双方主机去识别
只需要转换IP和port就可以,不需要转换传输的数据包的字节序
因为IP和port为 4个字节和2个字节, 而数据报一般都为char类型, 占一个字节
根据字节序的性质,内存存储大于一个字节类型的数据在内存中的存放顺序。
所以char类型并不具有字节序的概念

判断当前主机字节序

数据类型强转 

#include <stdio.h>
int main() {
    int a = 0x12345678;
    char b;
    b = (char)a;
    printf("%#x\n", b);
    if (b == 0x78)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}

指针强转

#include <stdio.h>
int main() {
    int a=0x12345678;
    char *p=(char*)&a;
    printf("%#x\n",*p);
    if (*p == 0x78)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}

端口转换 8888
主机字节序转换为网络字节序 (小端序->大端序)
u_long htonl (u_long hostlong); //host to internet long
u_short htons (u_short short); //服务器、客户端写地址时用到
网络字节序转换为主机字节序(大端序->小端序)
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);//printf时用到

IP地址转换

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

TCP

TCP流程

服务器:------------------------------------------------------------------》接电话者
1. 创建流式套接字(socket)---------------------------------------------》有手机
2. 指定网络信息-----------------------------------------------------------》有号码
3. 绑定套接字(bind)----------------------------------------------------》绑定手机
4. 监听套接字(listen)---------------------------------------------------》待机
5. 接收客户端请求(accept)---------------------------------------------》接电话
6. 发送、接收消息(send recv)------------------------------------------》通话
7. 关闭套接字(close)------------------------------------------ ---------》挂断电话
客户端:-------------------------------------------------------------------》拨打电话者
1. 创建流式套接字(socket)----------------------------------------------》有手机
2. 指定网络(服务器)信息-------------------------------------------------》有对方号码
3. 请求连接(connect)----------------------------------------------------》打电话
4. 发送、接收消息(send/recv)-------------------------------------------》通话
5. 关闭套接字(close)-----------------------------------------------------》挂断电话

函数接口

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

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

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

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

5. recv

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

6. connect

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

7. send

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
    sockfd:socket函数的返回值
    buf:发送内容存放的地址
    len:发送内存的长度
    flags:如果填0,相当于write();
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    // 1.创建流式套接字(socket)--------------------------------------------》有手机
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd); // 3
    // 2.指定网络信息-------------------------------------------------------》有号码
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(8867); // 端口号
    saddr.sin_addr.s_addr = inet_addr("192.168.51.16");
    // 3.绑定套接字(bind)------------------------------------------------》绑定手机
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");
    // 4.监听套接字(listen)------------------------------------------------》待机
    // 将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");
    // 5.接收客户端请求(accept)-------------------------------------------》接电话
    int acceptfd = accept(sockfd, NULL, NULL);
    if (acceptfd < 0)
    {
        perror("accpet err");
        return -1;
    }
    printf("acceptfd:%d\n", acceptfd);

    // 6.发送、接收消息(send recv)----------------------------------------》通话
    // recv--->read
    int ret;
    while (1)
    {
        ret = recv(acceptfd, buf, sizeof(buf), 0);
        // 当接受成功:独到的个数
        // 0:客户端退出
        //-1:接受失败
        if (ret < 0)
        {
            perror("recv err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("client exit\n");
            break;
        }
        else
            printf("buf:%s\n", buf);
    }

    //  7.关闭套接字(close)----------------------------------------------------》挂断电话
    close(acceptfd);
    close(sockfd);
    return 0;
}

UDP

流程

服务器:--------------------------------------------------》短信的接收者
1. 创建数据报套接字(socket)---------------------------》有手机
2. 填充网络信息-------------------------------------------》有号码
3. 绑定(bind)--------------------------------------------》绑定手机
4. 接收发送消息(recvfrom sendto)---------------------》接收信息
5. 关闭套接字(close)--------------------------------------》接收完毕
客户端:---------------------------------------------------》短信的发送者
1. 创建数据报套接字(socket)--------------------------》有手机
2. 填充网络(服务器)信息---------------------------------》有对方号码
3. 接收发送消息(recvfrom sendto)---------------------》发信息
4. 关闭套接字(close)---------------------------------------》发送完毕

函数接口

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

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

TCP 框架 服务器/客户端

//创建流式套接字 socket
//指定网络信息
//绑定套接字 bind
//监听套接字 listen 
//接受客户端请求 accept
//发送、接受消息 send recv
//关闭套接字 close
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret, acceptfd;
    // 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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");
    while (1)
    {
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        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)
//创建流式套接字 socket
//指定网络信息
//请求连接 connect
//发送、接受消息 send recv
//关闭套接字 close

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

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    // 创建流式套接字
    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(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);
    }
    close(sockfd);

    return 0;
}

UDP 框架 服务器/客户端

//创建数据报套接字 socket
//填充网络信息
//绑定套接字 bind
//接受发送消息 recvfrom sendto
//关闭套接字 close
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 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)
}
创建数据报套接字 socket
填充网络信息
接受发送信息 recvfrom send
关闭套接字 close

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

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret = 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]);

    // 3.循环发送消息
    while (1)
    {

1、对于TCP是先运行服务器,客户端才能运行。
2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。

linux IO模型

阻塞IO:(阻塞函数)一直阻塞,等待到所需要的文件描述符有事件发生,才会返回
非阻塞IO:不等待,函数立即返回,没有事件发生(返回错误信息),有事件发生(要执行的事件)
信号驱动IO:异步,当监听的文件描述符有事件发生,内核会给进程的发送SIGIO信号,通知进程,进程就可以捕捉信号做相应的逻辑处理

阻塞IO

最常见、效率低、不浪费cpu
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。

学习的读写函数在调用过程中会发生阻塞相关函数如下:
读操作中的read、recv、recvfrom
读阻塞:需要读缓冲区中有数据可读,读阻塞解除
写操作中的write、send
写阻塞:阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。

注意:sendto没有写阻塞
1)sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。

其他操作:accept、connect
粘包、拆包 丢包
tcp粘包

tcp拆包

udp丢包

UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接收缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。
丢包出现原因: 接收缓存区满 网络拥堵, 传输错误
相比之下,TCP是一种面向连接的传输协议,它需要保证数据的可靠性和顺序性。TCP有发送缓存区和接收缓存区, 如果发送频率过快, 且内容小于发送缓存区的大小 , 可能会导致多个数据的粘包。如果发送的数据大于发送缓存区, 可能会导致拆包。
udp不会发生粘包和拆包,tcp不会丢包
UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将UDP的每个数据包分隔开, 不会造成粘包。
TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;
TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)

非阻塞IO

轮询、耗费cpu,可以处理多路IO
•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。

设置非阻塞式IO的方式

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

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

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.将修改好的权限设置回去

信号驱动IO

异步通知模式,需要底层驱动支持
异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。
1)通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
2)应用程序收到信号后做异步处理即可。
3)应用程序需要把自己的进程号告诉内核,并打开异步通知机制。

//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)

超时检测

概念
什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理
比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;

必要性
1. 避免进程在没有数据时无限制的阻塞;
2. 规定时间未完成语句应有的功能,则会执行相关功能;

IO多路复用

应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;
若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
比较好的方法是使用I/O多路复用技术。其(select)基本思想是:
先构造一张有关描述符的表(最大1023),然后调用一个函数。
当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。

select

特点
1. 一个进程最多能监听1024个文件描述符
2. select每次被唤醒,都要重新轮询表,效率低
3. select每次都清空未发生响应的文件描述符,每次都要重新拷贝用户空间的表到内核空间

编程步骤
1. 创建表
2. 清空表
3. 将关心的文件描述符添加到表中
4. select
5 . 判断
6. 处理

函数接口

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

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

用select创建并发服务器,可以同时连接多个客户端 (监听键盘和sockfd)

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

int main(int argc, char const *argv[])
{
    int ret, maxfd, acceptfd;
    char buf[32] = {0};
    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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");
    maxfd = sockfd;
    // 1.构造一张关于文件描述符的表
    fd_set rdfs;
    fd_set tempfs;
    // 2.清空表FD_ZERO
    FD_ZERO(&rdfs);
    FD_ZERO(&tempfs);
    // 3.将关心的文件描述符添加到表中FD_SET
    FD_SET(0, &rdfs); // 键盘
    FD_SET(sockfd, &rdfs);
    while (1)
    {
        tempfs=rdfs;
        // 4.调用select函数,监听
        ret = select(maxfd + 1, &tempfs, NULL, NULL, NULL);
        if (ret < 0)
        {
            perror("select errr");
            return -1;
        }
        // 5.判断到底是哪一个或者哪些文件描述符发生的事件FD_ISSET
        if (FD_ISSET(0, &tempfs))
        {
            // 6.做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("buf:%s\n", buf);
        }
        if (FD_ISSET(sockfd, &tempfs))
        {
            acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
            if (acceptfd < 0)
            {
                perror("accept err");
                return -1;
            }
            printf("ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
        }
        memset(buf, 0, sizeof(buf));
    }

    close(sockfd);

    return 0;
}

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

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

int main(int argc, char const *argv[])
{
    int ret, maxfd, acceptfd, rev;
    char buf[32] = {0};
    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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
}

poll

特点
1. 优化了文件描述符的限制
2. poll每次被唤醒,需要重新轮询,效率比较低,耗费cpu
3. poll不需要构造文件描述符的表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间

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

函数接口

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 出错		>0 表示有事件产生;	   ==0 表示超时时间已到;	

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

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

int main(int argc, char const *argv[])
{
    int ret;
    char buf[128] = {0};
    int fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd:%d\n", fd);
    // 1.创建结构体数组
    struct pollfd fds[2];
    // 2.将关心的文件描述符添加到结构体数组中,并赋予事件
    fds[0].fd = fd;         // 鼠标
    fds[0].events = POLLIN; // 想要发生的事件
    fds[1].fd = 0;          // 键盘
    fds[1].events = POLLIN; // 想要发生的事件
    // 3.保存数组内最后一个有效元素下标
    int maxfd = 1;
    // 4.调用poll函数,监听
    while (1)
    {
        ret = poll(fds, maxfd + 1, -1);
        if (ret < 0)
        {
            perror("poll err");
            return -1;
        }
        // 5.判断结构体内文件描述符实际触发的事件
        if (fds[0].revents == POLLIN)
        {
            // 6.根据不同文件描述符触发的不同事件,做对应的逻辑处理
            read(fd, buf, sizeof(buf));
            printf("mouse:%s\n", buf);
        }
        if (fds[1].revents == POLLIN)
        {
            fgets(buf, sizeof(buf), stdin);
            printf("buf:%s\n", buf);
        }
        memset(buf, 0, sizeof(buf));
    }
    close(fd);
    return 0;
}

epoll详解

特点
1. 监听的文件描述符没有了个数的限制(取决于自己系统)
2. 异步IO,epoll当有事件唤醒,文件描述符会主动的调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高
3. epoll不需要构造文件描述符的表。只需要从用户空间到内核空间拷贝一次

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

epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;
eg:1GB机器上,这个上限10万个左右。
每个fd上面有callback(回调函数)函数,只有产生事件的fd才有主动调用callback,不需要轮询。
注意:
Epoll处理高并发,百万级
1. 红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)
2. 就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件)

函数接口

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

总结

select

poll

epoll

监听个数

一个进程最多监听1024个文件描述符(128字节)

由程序员自己决定

百万级

方式

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

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

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

效率

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

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

不轮询,效率高

原理

每次使用select后,都会清空表
每次调用select,都需要拷贝用户空间的表到内核空间
内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环

不会清空结构体数组
每次调用poll,都需要拷贝用户空间的结构体到内核空间
内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环

不会清空表
epoll中每个fd只会从用户空间到内核空间只拷贝一次(epoll_ctl添加ADD时)
通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝

特点

1. 一个进程最多能监听1024个文件描述符
2. select每次被唤醒,都要重新轮询表,效率低

1. 优化文件描述符的个数限制
2. poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu)

1. 监听的文件描述符没有个数限制(取决于自己的系统)
2. 异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高

结构

文件描述符表(位表)

结构体数组

红黑树和就绪链表

开发复杂度

阻塞式IO

不轮询,效率低,不浪费cpu

非阻塞IO

轮询,耗费cpu,可以处理多路IO

信号驱动IO/异步IO

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

IO多路复用

可以同时处理多路IO,效率高

服务器模型

在网络通信中,通常一个服务器要连接多个客户端
为了处理多个客户端的请求,服务器的程序就有多种表现形式

循环服务器
一个服务器在同一时间只能链接一个客户端

tcp:
socket();//流式
指定信息
bind();
listen();
while(1)
{
    accept();
    while(1)
        recv();
}
close();

udp:
socket();//数据报
指定网络
bind();
while(1)
    recvfrom()/sendto();
close();

并发服务器
一个服务器在同一时间能同时处理多个客户端请求

多进程
accept之后fork
子进程:通信
父进程:循环等待链接
父进程中进行子进程回收,waitpid
信号:SIGCHLD:当子进程状态改变给父进程发送的信号
优点: 服务器更稳定 , 父子进程资源独立, 安全性高一点
缺点: 需要开辟多个进程,大量消耗资源

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret, acceptfd;
    pid_t pid;
    // 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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");

    signal(SIGCHLD, handler);

    while (1)
    {
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        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)
        {
            // 子进程
            //  接受消息
            while (1)
            {
                ret = recv(acceptfd, buf, sizeof(buf), 0);
                if (ret < 0)
                {
                    perror("recv err");
                    break;
                }
                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);
    }
    close(sockfd);
    return 0;
}

多线程
每有一个客户端连接,就开辟一个线程,与客户端通信
accept之后pthread_create
主:循环等待连接
子:通信
优点: 相对多进程, 资源开销小, 线程共享同一个进程的资源
缺点: 需要开辟多个线程,而且线程安全性较差

void *handler(void *arg)
{
    // 通信
    int ret;
    char buf[128] = {0};
    int acceptfd = *((int *)arg);
    //  接受消息
    while (1)
    {
        ret = recv(acceptfd, buf, sizeof(buf), 0);
        if (ret < 0)
        {
            perror("recv err");
            return 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;
    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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");
    while (1)
    {
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        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;
}

IO多路复用
优点: 节省资源, 减小系统开销,性能高;
缺点: 代码复杂性高

FTP项目

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

void list(int acceptfd);
void putfile(int acceptfd, char *p);
void getfile(int acceptfd, char *p);

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret, acceptfd;
    // 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 = INADDR_ANY; // 0.0.0.0
    int len = sizeof(caddr);
    // 绑定
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");
    while (1)
    {
        acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        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);
                if (!strcmp(buf, "list"))
                    list(acceptfd);
                else if (!strncmp(buf, "put ", 4))
                    putfile(acceptfd, buf);
                else if (!strncmp(buf, "get ", 4))
                    getfile(acceptfd, buf);
            }

            memset(buf, 0, sizeof(buf));
        }
        close(acceptfd);
    }
    close(sockfd);

    return 0;
}
/*列出服务器路径下除目录外的所有文件,并发送给客户端*/
void list(int acceptfd)
{
    struct dirent *p;
    struct stat st;
    // 打开目录
    DIR *dir = opendir(".");
    if (dir == NULL)
    {
        perror("opendir err");
        return;
    }
    // 读目录
    while ((p = readdir(dir)) != NULL)
    {
        // 判断是否为目录文件
        stat(p->d_name, &st);
        if ((st.st_mode & S_IFMT) != S_IFDIR)
            // 发送非目录文件
            send(acceptfd, p->d_name, sizeof(p->d_name), 0);
    }
    send(acceptfd, "end", 128, 0);
    closedir(dir);
}

void putfile(int acceptfd, char *p)
{
    char buf[128] = {0};
    int ret;
    // 打开文件
    int fd = open(p + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open err");
        return;
    }
    // 循环接受客户端发来的文件内容,写到目标文件里
    while (1)
    {
        ret = recv(acceptfd, buf, 128, 0);
        if (ret < 0)
        {
            perror("recv file err");
            return;
        }
        else
        {
            if (!strcmp(buf, "end"))
                break;
            printf("%s", buf);
            write(fd, buf, strlen(buf));
            memset(buf, 0, sizeof(buf));
        }
    }
    close(fd);
    return;
}

void getfile(int sockfd, char *p)
{
    char buf[128] = {0};
    ssize_t s;
    // 打开文件
    int fd = open(p + 4, O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return;
    }
    // 循环读源文件,发送给服务器
    while (1)
    {
        s = read(fd, buf, sizeof(buf) - 1);
        buf[s] = '\0';
        if (s == 0)
        {
            printf("send end\n");
            send(sockfd, "end", 128, 0);
            break;
        }
        else
        {
            printf("%s", buf);
            send(sockfd, buf, sizeof(buf), 0);
        }

        memset(buf, 0, sizeof(buf));
    }
    close(fd);
    return;
}
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

void list(int sockfd);
void show();
void putfile(int sockfd, char *p);
void getfile(int sockfd, char *p);

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    // 创建流式套接字
    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(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)
    {
        show();
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        send(sockfd, buf, sizeof(buf), 0);
        if (!strcmp(buf, "list"))
            list(sockfd);
        else if (!strncmp(buf, "put ", 4))
            putfile(sockfd, buf);
        else if(!strncmp(buf, "get ", 4))
            getfile(sockfd, buf);
        else if (!strcmp(buf, "exit"))
            break;
    }
    close(sockfd);
    return 0;
}

void show()
{
    printf("----------------list----------------\n");
    printf("------------put filename------------\n");
    printf("------------get filename------------\n");
    printf("----------------exit----------------\n");
}

void list(int sockfd)
{
    char buf[128] = {0};
    int ret;
    while (1)
    {
        ret = recv(sockfd, buf, sizeof(buf), 0);
        if (ret < 0)
        {
            perror("recv err");
            return;
        }
        else
        {
            if (!strcmp(buf, "end"))
                break;
            printf("%s ", buf);
        }
        memset(buf, 0, sizeof(buf));
    }
    putchar(10);
    return;
}
/*上传客户端路径下的文件到服务器路径下*/
void putfile(int sockfd, char *p)
{
    char buf[128] = {0};
    ssize_t s;
    // 打开文件
    int fd = open(p + 4, O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return;
    }
    // 循环读源文件,发送给服务器
    while (1)
    {
        s = read(fd, buf, sizeof(buf) - 1);
        buf[s] = '\0';
        if (s == 0)
        {
            printf("send end\n");
            send(sockfd, "end", 128, 0);
            break;
        }
        else
        {
            send(sockfd, buf, sizeof(buf), 0);
        }

        memset(buf, 0, sizeof(buf));
    }
    close(fd);
    return;
}

void getfile(int acceptfd, char *p)
{
    char buf[128] = {0};
    int ret;
    // 打开文件
    int fd = open(p + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open err");
        return;
    }
    // 循环接受客户端发来的文件内容,写到目标文件里
    while (1)
    {
        ret = recv(acceptfd, buf, 128, 0);
        if (ret < 0)
        {
            perror("recv file err");
            return;
        }
        else
        {
            if (!strcmp(buf, "end"))
                break;
            write(fd, buf, strlen(buf));
            memset(buf, 0, sizeof(buf));
        }
    }
    close(fd);
    return;
}

setsockopt

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

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

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

设置地址重用

设置超时检测

通过函数自带参数设置

select:最后一个参数,通过结构体设置
poll:最后一个参数,毫秒级别
epoll_wait:最后一个参数,毫秒级别

setsockopt


image.png


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设置为指示错误
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

//SIGCHLD

void handler(int sig)
{
    printf("time out......\n");
}

int main(int argc, char const *argv[])
{
    // fcntl
    struct sigaction act;
    sigaction(SIGALRM, NULL, &act);//获取原来的属性
    act.sa_handler = handler;//修改
    sigaction(SIGALRM, &act, NULL);//设置新的属性

    char buf[32] = {0};
    while (1)
    {
        alarm(2);
        if (read(0, buf, 32) < 0)
        {
            printf("errr\n");
            continue;
        }
        printf("buf:%s\n", buf);
    }

    return 0;
}

广播与组播(UDP)

1. 广播
● 前面介绍的数据包发送方式只有一个接受方,称为单播
● 如果同时发给局域网中的所有主机,称为广播
● 只有用户数据报(使用UDP协议)套接字才能广播
● 一般被设计成局域网搜索协议
● 广播地址:192.168.51.255
发送者(udp客户端)
1. 创建数据报套接字
2. 由于套接字原本的属性不允许广播,所以要给套接字设置广播属性
3. 填充网络信息
4. 发送消息
5. 关闭套接字

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

int main(int argc, char const *argv[])
{
    char buf[32] = {0};
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err\n");
        return -1;
    }
    // 设置套接字属性:广播属性
    int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval));
//sockfd: 这是套接字描述符,是在之前用 socket() 函数创建的。
//SOL_SOCKET: 这是 setsockopt 的层参数,表示要设置的选项属于套接字级别。
//SO_BROADCAST: 这是选项参数,表示设置广播选项。
//&optval: 这是一个指向 optval 的指针,表示要设置的选项的值。在这个例子中,optval 是 1,表示启用广播。
//sizeof(optval): 这个参数指定了 optval 的大小。
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("192.168.51.255");
    int len = sizeof(caddr);
    while (1)
    {
        fgets(buf, sizeof(buf), stdin);
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr,len);
    }
    close(sockfd);

    return 0;
}

接收者(udp服务器)
1. 创建数据报套接字
2. 填充结构体信息(~.255)
3. 绑定
4. 接收消息
5. 关闭套接字
缺点:
广播方式发给所有的主机,过多的广播会大量的占用网络带宽,造成广播风暴,影响正常的通信
广播风暴:
过多的广播大量的占用网络带宽,无法正常进行点对点通信
网络长时间被大量的广播数据包所占用,使正常的点对点通信无法正常进行,其外在表现为网络速度奇慢无比,甚至导致网络瘫痪

组播(多播)

● D类:224.0.0.0-239.255.255.255
● 单播方式只能发给一个接收方。
● 广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。
● 组播是一个人发送,加入到多播组的人接收数据。
● 多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
发送者(udp客户端)
1. 创建数据报套接字
2. 指定网络信息
3. 发送消息
4. 关闭套接字
接收者(udp服务器)
1. 创建数据报套接字
2. 设置多播属性,将自己的IP加入多播组
3. 指定网络信息
4. 绑定
5. 接收消息
6. 关闭套接字

#include <stdio.h>
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <linux/in.h>
int main(int argc, char const *argv[])
{
    char buf[32] = {0};
    // udp服务器
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }

    设置多播属性
    struct ip_mreq req;
    req.imr_multiaddr.s_addr = inet_addr(argv[1]); // 组播IP
    req.imr_interface.s_addr = INADDR_ANY;         // 自己的IP

    setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req));

    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]);
    int len = sizeof(caddr);
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    int ret;
    while (1)
    {
        ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&caddr, &len);
        if (ret < 0)
        {
            perror("recvfrom err");
            return -1;
        }
        else
        {
            printf("%s\n", buf);
        }
        memset(buf, 0, sizeof(buf));
    }
    return 0;
}

本地套接字

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

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

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    // 创建流式套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定网络信息
    struct sockaddr_un saddr;
    saddr.sun_family = AF_UNIX;
    strcpy(saddr.sun_path, "./myunix");
    // 链接
    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);
    }
    close(sockfd);

    return 0;
}

服务器 流程(流式)
1. socket
2. sockaddr_un
3. bind
4. listen
5. accept
6. recv
7. close

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

int main(int argc, char const *argv[])
{
    char buf[128] = {0};
    int ret, acceptfd;
    // 1创建套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);
    // 指定网络信息
    struct sockaddr_un saddr;
    saddr.sun_family = AF_UNIX;
    strcpy(saddr.sun_path, "./myunix");

    // 删除文件
    //system("rm ./myunix");
    unlink("./myunix");//专门删除文件

    // 绑定:创建并打开套接字文件
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    // 监听:将主动套接字变为被动套接字
    if (listen(sockfd, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen okk\n");
    while (1)
    {
        acceptfd = accept(sockfd, NULL, NULL);
        if (acceptfd < 0)
        {
            perror("accept err");
            return -1;
        }
        // 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;
}

三次握手与四次挥手

三次握手
在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状态,完成三次握手。

客户端的初始序列号为J,而服务器的初始序列号为K。在ACK里的确认号为发送这个ACK的一端所期待的下一个序列号。因为SYN只占一个字节的序列号空间,所以每一个SYN的ACK中的确认号都是相应的初始序列号加1.类似地,每一个FIN(表示结束)的ACK中的确认号为FIN的序列号加1.
完成三次握手,客户端与服务器开始传送数据,在上述过程中还有一些重要概念。
未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户端确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户端确认包时,删除该条目,服务器进入ESTABLISHED状态。

为什么式三次握手,而不是两次握手?
主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而导致不必要的错误和资源的浪费。
两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据。
举例:
假如说有两次连接
第一次连接
第一次握手:客户端发送SYN,由于网络拥堵没有及时地到达服务器
客户端此连接关闭
第二次连接
第一次握手:客户端发送SYN,第一次连接的握手包也到达了服务器,第一次连接的第二次握手成功服务回复ACK消息
现在服务器认为建立了链接,客户端认为没有建立连接
现在第二次连接的第一次握手包也到达了服务器
第二次握手:服务器收到SYN包之后,回复ACK确认包给客户端
服务器认为建立了两次连接,但是客户端只认为建立了一次

四次挥手

image.png


第一次挥手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
第二次挥手:接收到FIN的另一端执行被动关闭(passive close)这个FIN由TCP确认。它的接收也作为文件结束符传递给接收端应用进程(放在已排队等候应用进程接收到任何其他数据之后)
第三次挥手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
第四次挥手:接收到这个FIN的原发送端TCP对它进行确认。

image.png

网络协议头

数据的封装与传递过程
思考:
1. 应用层调用send后,是如何把数据发送到另一台机器的某个进程的
2. 接收的设备收到数据包后,如何处理给应用层?

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

以太网帧完整帧

● 对于网络层最大数据帧长度是1500字节
● 对于链路层最大数据长度是1518字节(1500+14+CRC)
● 发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递

TCP粘包、拆包发生原因
发生TCP粘包或拆包有很多原因,常见的几点:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(传输层的最大报文长度),将进行拆包(到网络层拆包 - id ipflags )。
3、要发送的数据小于TCP发送缓冲区大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

image.png


粘包解决办法:
解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
4. 延时发送

https://www.cnblogs.com/111testing/p/12810253.htmlicon-default.png?t=N7T8https://www.cnblogs.com/111testing/p/12810253.html以太网头部

image.png


IP头


image.png

IHL:数据流量控制

image.png


面试:用于拆包、组包的头? IP头 (网络层)
IP哪些内容用于拆包? ID flag
IP里面包含了两个地址,哪两个?源地址,目标地址

TCP头

UDP头

image.png


UDP不会造成粘包和拆包, TCP不会造成丢包
UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将数据包分隔开, 不会造成粘包。
UDP不会造成拆包, 但会出现拆包, 这个拆包是在网络层的IP头进行的拆包(判断MTU)。
TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;
TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)
为什么UDP会造成丢包:
UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。
丢包出现原因: 接收缓存区满 网络拥堵, 传输错误

wireshark抓包

安装

虚拟机

安装:sudo apt-get install wireshark
启动:sudo wireshark

windows

关闭防火墙,可以直接默认安装

打开抓包工具,选择网卡

windows下:

image.png

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

image.png


过滤其他无关的包,命令可以选择:tcp.port==8888 and ip.addr=="192.168.51.198"


image.png




image.png

SQL数据库

常用的数据库

大型数据库 :Oracle
中型数据库 :Server是微软开发的数据库产品,主要支持windows平台
小型数据库 : mySQL是一个小型关系型数据库管理系统。开放源码 (嵌入式不需要存储太多数据)

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

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

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

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

4. 跨平台支持:SQLite在各种操作系统上都能够运行,而MySQL需要在特定的操作系统上安装和配置数据库服务器。
总之,MySQL适用于大型应用程序和网站,需要处理大量数据和高并发访问,而SQLite适用于小型应用程序或移动设备,对性能和规模要求没有那么高。

SQL基础

SQLite的源代码是C,其源代码完全开放。它是一个轻量级的嵌入式数据库。
SQLite有以下特性:
        零配置一无需安装和管理配置;
        储存在单一磁盘文件中的一个完整的数据库;
        数据库文件可以在不同字节顺序的机器间自由共享;
        支持数据库大小至2TB(1024G = 1TB);//嵌入式足够
        足够小,全部源码大致3万行c代码,250KB;
        比目前流行的大多数数据库对数据的操作要快;
创建:
手动:
        使用sqlite3工具,手工输入命令
        命令行输入
代码:
        利用代码编程,调用接口

虚拟机中安装sqlite3

源码安装:
tar xf sqlite-autoconf-3140100.tar.gz
cd sqlite-autoconf-3140100
./configure
make
sudo make install
安装完成后测试安装是否成功
sqlite3 -version
3.11.0 2016-02-15 17:29:24 3d862f207e3adc00f78066799ac5a8c282430a5f
图形化:
sudo apt-get install sqlitebrowser

基础SQL语句使用

命令的方式操作

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

(创建一个新的数据库)

两种命令:

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

都是以 '.' 开头的

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

b. .quit 退出

c. .tables 查看有哪些表

d. .schema stu2 查看表结构

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

SQL命令是以 “;” 结尾

在库当中创建一个表

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

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

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

2 删除一个表

drop table <table_name>

...>;

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')

4 查找数据

查询表中所有记录

select * from <table_name>;

(*表示查询所有的值)

按指定条件查询表中记录

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从大到小进行排序

5 修改(更新)数据

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

update stu set id=10 where id=1;

6 增加字段

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

alter table stu add column class int default 1;

(表示添加了一列class,默认值为1)

7 删除字段(在数据库当中其实不支持直接删除一个字段(及一列),

如果就想删除一列,那么需要三步骤)

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

2) drop table stu;删除原有的stu表

 3) alter table student rename to stu; 重命名

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

8》删除一行

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

或者

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

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

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

sqlite3编程

官方文档:List Of SQLite Functions

头文件:#include <sqlite3.h>

编译:gcc sqlite1.c -lsqlite3

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

使用:   fprintf(stderr,"sqlite3_open failed %s\n",sqlite3_errmsg(db));
关闭数据库
int sqlite3_close(sqlite3 *db);
功能:关闭sqlite数据库
返回值:成功返回SQLITE_OK,失败返回错误码
执行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 = NULL;
    // 打开数据库
    if (sqlite3_open("./stu.db", &db) != SQLITE_OK)
    {
        // printf("stdout:%s\n",sqlite3_errmsg(db));
        fprintf(stderr, "open err:%s\n", sqlite3_errmsg(db));
        return -1;
    }
    printf("open ok\n");

    // 数据库操作
    char *errmsg = NULL;
    // 创建一张表
    if (sqlite3_exec(db, "create table stu(id int,name char,score float);",
                     NULL, NULL, &errmsg) != SQLITE_OK)
    {
        fprintf(stderr, "create err:%s\n", errmsg);
        // return -1;
    }
    printf("create table ok\n");
    // 添加数据
    //  if (sqlite3_exec(db, "insert into stu values(1,\"xiaodou\",69.99);",
    //                   NULL, NULL, &errmsg) != SQLITE_OK)
    //  {
    //      fprintf(stderr, "insert err:%s\n", errmsg);
    //      //return -1;
    //  }
    //  printf("insert ok\n");
    //  if (sqlite3_exec(db, "insert into stu values(2,\"ayuan\",70.00);",
    //                   NULL, NULL, &errmsg) != SQLITE_OK)
    //  {
    //      fprintf(stderr, "insert err:%s\n", errmsg);
    //      //return -1;
    //  }
    //  printf("insert ok\n");
    int num = 0, id;
    char name[32] = {0};
    float score;
    printf("请输入学生人数:");
    scanf("%d", &num);
    char sql[128] = {0};
    for (int i = 0; i < num; i++)
    {
        printf("请输入学生学号 姓名 成绩:\n");
        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 ok\n");
    }

    // 查找数据
    // if (sqlite3_exec(db, "select * from stu;", callback, "hello", &errmsg) != SQLITE_OK)
    // {
    //     fprintf(stderr, "select err:%s\n", errmsg);
    //     return -1;
    // }
    char **result = NULL;
    int row = 0;    // 存放有几行符合条件的信息
    int column = 0; // 列数
    sqlite3_get_table(db, "select * from stu where id=1;", &result, &row, &column, &errmsg);
    int k = 0;
    printf("row:%d column:%d\n", row, column);
    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)
{
    // 每查找到一次符合要求的数据便会调用一次callback回调函数
    static int d = 1;
    printf("buf:%s d:%d\n", (char *)buf, d++);
    // num:列数
    printf("num:%d\n", num);
    // name:列名
    for (int i = 0; i < num; i++)
        printf("%s ", name[i]);
    putchar(10);
    // value:内容
    for (int i = 0; i < num; i++)
        printf("%s ", value[i]);
    putchar(10);
    printf("----------------------\n");
    return 0; // 必须带,不然查找会报错
}

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("fork 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(saddr));
        }
    }
    else
    {
        while (1)
        {
            if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len) < 0)
            {
                perror("recvfrom err");
                return -1;
            }
            printf("msg.name:%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)
{
    sprintf(msg.text, "%s login.....", msg.name);
    printf("%s\n", msg.text);
    while (p->next != NULL)
    {
        p = p->next;
        sendto(sockfd, &msg, sizeof(msg), 0,
               (struct sockaddr *)&(p->addr), sizeof(p->addr));
    }
    link_t *pnew = (link_t *)malloc(sizeof(link_t));
    pnew->addr = caddr;
    pnew->next = NULL;
    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)) != 0)
            sendto(sockfd, &msg, sizeof(msg), 0,
                   (struct sockaddr *)&(p->addr), sizeof(p->addr));
    }
    return;
}
// 客户端退出功能:将用户下线消息发送给其他用户,将下线用户从链表中删除
void client_quit(int sockfd, link_t *p, MSG_t msg, struct sockaddr_in caddr)
{
    sprintf(msg.text, "%s quit.....", msg.name);
    printf("%s\n", msg.text);
    link_t *pdel = NULL;
    while (p->next != NULL)
    {
        if (!memcmp(&(p->next->addr), &caddr, sizeof(caddr)))
        {
            pdel = p->next;
            p->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }
        else
        {
            p = p->next;
            sendto(sockfd, &msg, sizeof(msg), 0,
                   (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
    }
    return;
}
#include "head.h"

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[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(saddr));

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0) // 循环接受服务器消息
    {
        while (1)
        {
            if (recvfrom(sockfd, &msg, sizeof(MSG_t), 0, NULL, NULL) < 0)
            {
                perror("recv from err");
                return -1;
            }
            else
            {
                if (msg.type == chat)
                    printf("%s says:%s\n", msg.name, msg.text);
                else
                    printf("%s\n", msg.text);
            }
        }
    }
    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 = quit;
                sendto(sockfd, &msg, sizeof(MSG_t), 0,
                       (struct sockaddr *)&saddr, sizeof(saddr));
                break;
            }
            else
            {
                msg.type = chat;
                sendto(sockfd, &msg, sizeof(MSG_t), 0,
                       (struct sockaddr *)&saddr, sizeof(saddr));
            }
        }
        kill(pid, SIGKILL);
        wait(NULL);
    }
    close(sockfd);
    return 0;
}

网编内容梳理

发展:阿帕网-----因特网-----互联网------物联网

局域网、广域网、光猫、路由器

IP:概念:标识、IPV4(32,点分十进制) IPV6(128),分类(A、B、C、D、E),

特殊地址

子网掩码:作用(区分网络号和主机号),特点(网络号全为1,主机号全为0)

三级划分:重新组网,提高资源利用率

网络模型:OSI(7层:应、表、会、传、网、数、物)、TCP/IP(4层:应、传、网、网和物)每一层的功能、每层协议、DOS

socket:是什么、类型(流式、数据报、原始)、位置(应用层与传输层之间)

端口(2个字节)、字节序(网络、主机)

TCP与UDP区别,TCP高可靠性是什么、为什么,

TCP客户端、服务器流程,UDP客户端、服务器流程

linux IO模型:阻塞IO(最常见、效率低、不耗费cpu)、非阻塞IO(轮询、耗费cpu、可以处理多路IO)(设置方式:函数自带参数、fcntl改变文件描述符的属性)、信号驱动IO(异步通知方式、需要底层驱动的支持)、IO多路复用(可以同时处理多路IO,效率高)(select(特点、编程步骤、实现并发服务器))poll(特点、编程步骤)epoll(特点)

服务器模型:循环服务器、并发服务器(多进程、多线程、IO多路复用(优缺点))

setsockopt:设置套接字属性(地址重用、超时检测、广播、组播)

超时检测:必要性,设置方法(参数、setsockopt、alarm函数与sigaction)

广播、组播:将广播的发送者设置为广播属性(广播风暴),将组播的接收者设置为组播属性

本地套接字:在本地形成一个文件,进行通信

网络协议头:数据的封装、传递过程,数据帧组成,以太网头、IP头、TCP头、UDP头

粘包、拆包

三次握手、四次挥手

sql数据库:增(create inset)删(drop、delete)改(update)查(select)

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值