Linux 高并发服务器实战 - 4 Linux网络编程

Linux 高并发服务器实战 - 4 Linux网络编程

C/S架构可以自己定协议,B/S架构的协议是固定的:http/https (无法传递大数据量)

B/S架构是请求-响应,需要动态刷新,效率低

IP地址32位,是4个字节

IP地址最后一个字节一般不用255,它一般是用来广播的,第一个字节一般也不用0,从1开始

127 开头用于回环测试。127.0.0.1 本机

ping -> 测试某个主机能否和当前主机连通

ping 127.0.0.1 和自己连通

端口号有两个字节

IP是用来找到计算机的,但是消息是在应用之间发的,我们还得给一个定好的端口(比方QQ是用的哪个端口)

端口在计算机内存中相当于一个缓冲区,(有读缓冲区、写缓冲区)

用端口号来标识缓冲区,有65535个

进程的端口号是唯一的,为什么不用进程号来标识呢?不能,因为进程号每次都是动态产生的不是固定的

服务端回给客户端数据的时候,端口号就是动态端口(动态分配的)

端口号:这块读/写缓冲区的唯一标识

一个应用程序可以由多个端口,不同的端口可以和不同的应用程序通信

物理层要把101010的信号改变为电信号,高低电频

接收方物理层一样,要给再变回来

主流:TCP/IP四层模型,里面有TCP/IP协议族

我们就是用sshd远程连接的linux服务器(其实也是客户端和服务器的通信,它的端口号是22)

我们填写了IP地址(表明服务器),端口号(表示它那个进程)

ARP协议 : 根据IP地址找MAC地址

RARP:根据MAC找IP

协议最终体现为在网络上传输的数据包的格式

TTL每经过一个路由器 -1,一开始默认64/128 减到0就丢失了

在这里插入图片描述

收到的那一方,通过TCP/UDP头部里的端口号去决定发给哪个应用

分用是根据头部中的信息

在这里插入图片描述

接收端那边拿到,如果目的地址不是自己,那就丢掉了。

如果是自己,因为类型是0x800,所以去掉头部后交给上层的IP模块去处理

到了网络层看目的IP和本人是不是一样的,一样就处理并去掉头部,不一样就扔掉

IP头部里也有协议号,标明上一层是TCP/UDP?

到了传输层,再去找有没有对应的端口号…

传给应用

在这里插入图片描述

问题:到底是怎么查找到目标端的IP地址的?

ARP协议 -> 根据IP找到MAC地址

在这里插入图片描述

在这里插入图片描述

注意目的MAC地址目前为空(里面都是0)响应之后才会填充上

在这里插入图片描述

要想发送出去ARP请求包,前面还要加以太网帧头(6+6+2 == 14字节) 尾:4字节

在这里插入图片描述

A发送ARP请求,会发送给整个子网的主机,每个主机收到请求后根据里面的目的端IP去应答,A主机就拿到它们的IP地址了

在这里插入图片描述

FF:FF:FF:FF是给这个网络里所有主机都发

在这里插入图片描述

B/C主机响应的时候,源地址,目的地址的MAC地址、IP地址都是清楚了的,都能填充的上

注意操作部分由1变成2,由请求变成应答

在这里插入图片描述

注意在实际使用时,上层的协议使用下层的协议帮我们去完成一些任务

socket 介绍

在这里插入图片描述

先写数据到写缓冲区,然后封装(应用层、传输、网络、接口…) 再分用并且到了读缓冲区

套接字通信分两部分:

  • 服务器端:被动接受连接,一般不会主动发起连接
  • 客户端:主动向服务器发起连接
    socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。

字节序

传输的时候是小端,解析的时候是大端?没规定好,没法正常的解码编码

字节序:字节在内存中存储的顺序。

小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的低位地址

大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的低位地址

#include <stdio.h>

int main() {

    //c语言中的union 是联合体,就是一个多个变量的结构同时使用一块内存区域,
    //区域的取值大小为该结构中长度最大的变量的值
    union {
        short value;    // 2字节
        char bytes[sizeof(short)];  // char[2]
    } test;

    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
        printf("大端字节序\n");
    } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
        printf("小端字节序\n");
    } else {
        printf("未知\n");
    }

    return 0;
}

解决字节序问题在传输前都要先转化为大端

h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short unsigned short
l - long unsigned int

在网络上传输的网络字节序都是大端,主机字节序有可能是小端也有可能是大端

#include <arpa/inet.h>
// 转换端口		端口2个字节
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP		IP是4个字节
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序

转主机字节序的时候,如果本身是大端,就不转了,是小端才转

注意!

注意buf本身是一个char指针,把它转为int * 再对他取值,就是取了四个字节的!

怎么再单独取出每个字节的地址?转为char,再以步长为1往前走*

// htonl  转换IP
    char buf[4] = {192, 168, 1, 100};
	//注意buf本身是一个char指针,把它转为int * 再对他取值,就是取了四个字节的!
    int num = *(int *)buf;
    int sum = htonl(num);
    unsigned char *p = (char *)&sum;

	printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
#include <stdio.h>
#include <arpa/inet.h>

int main() {

    // htons 转换端口
    unsigned short a = 0x0102;
    printf("a : %x\n", a);
    unsigned short b = htons(a);
    printf("b : %x\n", b);

    printf("=======================\n");

    // htonl  转换IP
    char buf[4] = {192, 168, 1, 100};
    int num = *(int *)buf;
    int sum = htonl(num);
    unsigned char *p = (char *)&sum;

    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

    printf("=======================\n");

    // ntohl
    unsigned char buf1[4] = {1, 1, 168, 192};
    int num1 = *(int *)buf1;
    int sum1 = ntohl(num1);
    unsigned char *p1 = (unsigned char *)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
    
     // ntohs


    return 0;
}

输出:

=======================
100 1 168 192
=======================
192 168 1 1

socket 地址

// socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
// 客户端 -> 服务器(IP, Port)

通用 socket 地址

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};

typedef unsigned short int sa_family_t;

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议
族(protocol family,也称 domain)和对应的地址族入下所示:

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混
用。

sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所
示:

协议族地址值含义和长度
PF_UNIX文件的路径名,长度可达到108字节
PF_INET16 bit 端口号和 32 bit IPv4 地址,共 6 字节
PF_INET616 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节

由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的
通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。

#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

专用socket地址

详见pdf, 从sockaddr 转变为了 sockaddr_in

注意,最后在用的时候,还是要转化成为sockaddr才行,强转即可

通用sockaddt里有一个 sa_famliy地址族类型的变量(存常见三种协议族),以及14个字节的数据存地址及端口号等,后面又有了专用的socket地址

in_addr_t一般为32位的unsigned int,用它存IPV4

IP地址转换

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
//是把转换后的值存在inp里了,不是返回值,返回值是表示是否成功
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的	但是dst也是必须要传的
#include <stdio.h>
#include <arpa/inet.h>

int main() {

    // 创建一个ip字符串,点分十进制的IP地址字符串
    char buf[] = "192.168.1.4";
    unsigned int num = 0;

    // 将点分十进制的IP字符串转换成网络字节序的整数
    inet_pton(AF_INET, buf, &num);
    unsigned char * p = (unsigned char *)&num;
    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));


    // 将网络字节序的IP整数转换成点分十进制的IP字符串
    // 定义16是因为 3 * 4 = 12 再加三个点 再加一个\0  -> 16
    char ip[16] = "";
    const char * str =  inet_ntop(AF_INET, &num, ip, 16);
    printf("str : %s\n", str);
    printf("ip : %s\n", str);
    // 注意返回值也是转换后的值,传入的参数ip 它里面对应的值也是改变了的
    printf("%d\n", ip == str);

    return 0;

TCP 通信流程

// TCP 和 UDP -> 传输层的协议
UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠
TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输
					 UDP 						TCP
是否创建连接 			无连接 	 				面向连接
是否可靠 		 	 不可靠 	   				   可靠的
连接的对象个数  一对一、一对多、多对一、多对多 		支持一对一
传输的方式 			面向数据报 				    面向字节流
首部开销 			  8个字节 					 最少20个字节
适用场景 		实时应用(视频会议,直播) 	 可靠性高的应用(文件传输)
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接

在这里插入图片描述

监听socket fd里面有没有数据,对面的写缓冲区写上,我们读入到读缓冲区,发现有了,就表示监听到了

在这里插入图片描述

accept 一开始阻塞,监听到有fd连接进来,才会放开

在这里插入图片描述

监听是监听的文件描述符,accept之后是返回新的fd,这个和客户端的进行通信,而不是用监听的去通信,它还得继续监听呢。等又有了新的,又accept,又建立新的fd,它得用于一直监听

// 客户端
1. 创建一个用于通信的套接字(fd)	(它不用绑定IP和端口号,三次握手之后双方的IP和端口就都知道了)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接

bind() 是把IP 和 Port 绑定到 上面监听socket fd上(需要有IP 和 Port 来标识套接字)

bind() 也可以称作socket命名

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5		(不需要指定太大)
    										(有未连接和已经连接的队列,accept 一但接收,已经连接的队列里就没有它了,这个执行的速度是非常快的,所以不用担心这个数字很小)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符(需要从读缓冲区里拿出来客户端的IP和Port)
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

TCP通信实现(服务器端)

计算机有多个网卡,以太网卡,无线网卡,他们的IP地址是不一样的,为保证他们都能访问到

那给socket绑定地址sockaddr_in时,将其IP(sin_addr.s_addr)设置为0(INADDR_ANY),就都能访问到了

// TCP通信:服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    
    //1.创建用于监听的socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    //2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    //3.监听
    ret = listen(lfd, 8);
    if(ret == -1)
    {
        perror("listen");
        exit(-1);
    }

    //4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &len);
    if(cfd == -1)
    {
        perror("accept");
        exit(-1);
    }
    

    //输出客户端信息
    char clientIP[16];
    //将网络字节序的整数,转化成点分十进制的字符串
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n",clientIP, clientPort);

    //5.通信
    char recvBuf[1024] = {0};
    while (1)
    {
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1)
        {
            perror("read");
            exit(-1);
        }else if(num > 0){
            printf("recv client data : %s\n", recvBuf);
        }else if(num == 0){
            //表示客户端断开连接
            printf("client closed..");
            break;
        }

        char* data = "hello, i am server";
        write(cfd, data, strlen(data));
    }
    
    //关闭文件描述符
    close(cfd);
    close(lfd);
    
    return 0;
}

TCP通信实现(客户端)

// TCP通信 客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    //1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(-1);
    }

    //2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.0.74", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if(ret == -1)
    {
        perror("connect");
    }

    //3.通信(不发数据的话,服务器端的read那里又会卡住)
    char recvBuf[1024] = {0};
    while (1)
    {
        char* data = "hello, i am client";
        write(fd, data, strlen(data));

        sleep(1);

        int num = read(fd, recvBuf, sizeof(recvBuf));
        if(num == -1)
        {
            perror("read");
            exit(-1);
        }else if(num > 0){
            printf("recv server data : %s\n", recvBuf);
        }else if(num == 0){
            //表示服务器端断开连接
            printf("server closed..");
            break;
        }
    }
    
    close(fd);

    return 0;
}

TCP三次握手

三次握手的目的是保证双方互相之间建立了连接

三次握手发生在客户端建立连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。

SYN -> 建立连接

ACK -> 确认

在这里插入图片描述

客户端和服务端都要确认双方能够收发信息

最后一次握手是为了让服务端确认 客户端可以收到数据

所以,为保证连接的可靠性至少是三次握手,两次握手不行

第一次:

​ S知道:S可收、C可发

​ C知道:C可发

第二次:

​ S知道: S可收可发、C可发

​ C知道:S可收可发、C可发可收

第三次:

​ S知道:S可收可发、C可发可收

在这里插入图片描述

在这里插入图片描述

序号:为每一个字节生成一个序号

确认序号

可以通过序号和确认序号保证数据的完整以及顺序

如何确保数据完整?

从110开始发,发了3个 seq = 110(3) , ack = 113 表示这收到了,并且下次希望从113开始收

注意

只有发送SYN / FIN时,确认序号是+1,其余的都是加具体数据的长度

一开始的序号seq是随机的

在这里插入图片描述

注意cack 后面一直是2001,因为服务端并没有再发数据

cack是客户端希望服务端下次从2001开始发

sack是服务端希望客户端下次从哪里开始发

(一开始三次握手的时候是没发数据的,只是SYN和ACK的响应)
第一次握手:
    1.客户端将SYN标志位置为1
    2.生成一个随机的32位的序号seq=J, 这个序号后边是可以携带数据(数据的大小)
第二次握手:
    1.服务器端接收客户端的连接,	ACK = 1
    2.服务器端回发一个确认号序号,ack = 客户端序号 + 数据长度 + SYN/FIN(1个长度)
    3.服务器端向客户端发起连接请求:SYN = 1
    4.服务器端生成一个随机序号 seq = K
第三次握手:
    1.客户端应答服务器的连接请求,ACK = 1
    2.客户端回发一个确认序号,ack = 服务器端序号 + 数据长度 +SYN/FIN(1)

!!!注意:只有第三次的时候是可以带一些数据的

滑动窗口

窗口理解为缓冲区的大小

滑动窗口的大小会随着发送数据和接收数据而变化

通信的双方都有发送缓冲区和接收数据的缓冲区

服务器:

​ 发送缓冲区(发送缓冲区的窗口)

​ 接收缓冲区(接收缓冲区的窗口)

客户端:

​ 发送缓冲区(发送缓冲区的窗口)

​ 接收缓冲区(接收缓冲区的窗口)

在这里插入图片描述

发送方的缓冲区
	白色格子:空闲空间
	灰色格子:数据已经被发送出去了,但是还没有被接收(因为还没有收到ACK)
	紫色格子:还没有发送出去的数据

接收方的缓冲区:
	白色格子:空闲的空间
	紫色格子:已经接受到的数据

接收缓冲区会告诉发送缓冲区我还剩多大,控制他那边发送的数量

在这里插入图片描述

# mss : Maximum Segment Size(一条数据的最大数据量)
# win : 滑动窗口
看4-9 是发送端往接收端发了6次,每次大小为1024 也就是1K,接收端滑动窗口满了
看10:接受到了前6145个数据,现在win窗口大小是2048(也就是说,我窗口里处理了2K了)
看11:win4096:处理了4K了,窗口现在是4096

1. 客户端向服务器发起连接,客户端的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3.第三次握手
4.4-9
5.第10次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6.第11次,服务器告诉客户端:发送的6k数据已经接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7.第12次,客户端给服务器发送了1k的数据
8.第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1K的数据
9.第14次,注意回复ack是8194不是8193(因为加了一个FIN),
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次挥手,客户端同意了服务器端断开的请求

四次挥手

在这里插入图片描述

四次挥手发生在断开连接的时候,在程序当中调用了close()会使用TCP协议进行四次挥手。
会释放IP、port...那些信息,以免占用内存资源
客户端和服务器端都可以主动发起断开连接,谁先调用close()就是谁发起
因为TCP在建立连接的时候,采用三次握手建立的连接是双向的,在断开的时候需要双向断开

注意主动断开连接的一方,断开之后不能发送数据了,但是还是可以接收数据的

为什么四次一方收到另一方FIN并发送ACK后不一并把自己的FIN发过去,变成三次挥手?因为你和我断开连接我同意了,但是我还没有要和你断开连接

TCP通信并发

要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。

思路:
   1.一个父进程,多个子进程
   2.父进程负责等待并接受客户端的连接
   3.

注意:

对于往socket里写数据:

  • 要么给往里写数据的字符串数组初始化为{0}
  • 要么在write的时候,把长度改为strlen(recvBuf) + 1,带上那个’\0’的位置 (使用这个)

另外不能再while循环里进行父进程对子进程的回收,一有wait,整个阻塞在那了,不能接收另一个客户端

用waitpid也不太好,所以我们用信号来完成释放

注意处理信号的handler函数要用while循环起来,搭配waitpid使用,有可能死了三个子进程,但是未决信号只有一个,我们用waitpid 一次是处理不完三个的

软中断

收到信号中断了,等再回来之后accept就不阻塞了,目前没新的客户端连接,它就报错了,所以我们处理一下

如果中断了(errno是 EINTR,那么就continue)

int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
if(cfd == -1) {
   	if(errno == EINTR) {
		continue;
    }
    perror("accept");
    exit(-1);
}

整体代码:

client:

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}

server:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {
    while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
            if(errno == EINTR) {
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }

    }
    close(lfd);
    return 0;
}

多线程实现并发服务器

线程的函数的参数是要以指针的形式传入,因为我们要传入很多,所以我们把它打包成一个结构体传入

又因为while函数里的变量是局部变量,我们传入线程函数后,它指针指向的内容已经释放了,所以我们把它存在堆里(用malloc),但是在子线程的函数结束后要把这部分空间释放。如果要有10000个线程同时连接,开销也是很大的

另外一种,我们可以把他们存在一个结构体数组里,记得要对他们初始化,fd初始化为-1,tid初始化为-1,表示他们是可用的

// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
	bzero(&sockinfos[i], sizeof(sockinfos[i]));
	sockinfos[i].fd = -1;
	sockinfos[i].tid = -1;
}

整体代码:

#define _XOPEN_SOURCE
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>


struct sockInfo
{
    int fd; //通信的文件描述符
    pthread_t tid; //线程号
    struct sockaddr_in addr;
};

struct sockInfo sockinfos[128];

void* working(void* arg)
{
    //子线程和客户端通信    要用到的参数:cfd 、客户端的信息、线程号
    //因为这些参数要以一个指针的形式传进来,所以我们需要定义一个结构体
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
            perror("read");
            exit(-1);
        }else if(len > 0) {
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main()
{
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }
    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        //注意这里结构体数据的复制是要用memcpy
        memcpy(&pinfo->addr, &cliaddr, len);

        // 创建子线程
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

TCP状态转换

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

没有接收到最后一次ACK,不是完整的断开连接

他会等2MSL,如果要没接受到,另一方会再发FIN,这一方再发一次ACK(丢失重发)。

2MSL之后这一端就关闭了

半关闭

从程序的角度,可以使用 API 来控制实现半连接状态:

#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发
出写操作。
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以
SHUT_WR。

半关闭时:A不能写了,但是还能读。但是如果用close,就整个都关了,读写都不行。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用
计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方
向的连接,只中止读或只中止写。
注意:

  1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用
    进程都调用了 close,套接字将被释放。
  2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。
    但如果一个进程 close(sfd) 将不会影响到其它进程。

端口复用

查看网络相关的命令:

netstat

​ 参数:

​ -a 所有的socket

​ -p 显示正在使用socket的程序的名称

​ -n 直接使用IP地址,不通过域名服务器

在这里插入图片描述

服务器重启了 /Ctrl + C停了、端口号还没有释放,此时再次启动服务器,他会报错说这个端口已经被用了,所以才使用了端口复用

这个时候是处于TIME_WAIT(虽然另一方还没给他发FIN)/FIN_WAIT_2阶段

在这里插入图片描述

端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口

#include <sys/types.h>
#include <sys/socket.h>
//不仅仅能设置端口复用,还可以设置套接字的属性
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t
optlen);
	参数:
         -sockfd : 要操作的文件描述符
         -level: 级别 - SOL_SOCKET (端口复用的级别)
         -optname: 选项的名称
             - SO_REUSEADDR
             - SO_REUSEPORT
         -optval : 端口复用的值(整形)
             - 1 : 可以复用
             - 0 : 不可以复用
         -optlen : optval参数的大小
             
端口复用,设置的时机是在服务器端口绑定前

IO多路复用

输入:从文件中把数据写到内存中

输出:从内存中把数据写到文件中

I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有 select、poll 和 epoll。

在这里插入图片描述

下面是server的fd使用情况,上面是client的fd使用情况

socket相当于流缓冲区,都通过各自socket来获取流中的数据。**2端socket通过一条固定“电话线”进行通讯,即Client端选择跟哪台Server,哪个端口建立连接;作为Server端只监听相应的端口。**在这里,Client处于主动。
建立连接后,两端都可以使用流通过socket相互的发送信息和接受信息。两端的socket都会不断刷新socket里的内容。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

提高了程序执行效率(不用阻塞了),缺点是需要更多资源(你要不断地打电话)

在这里插入图片描述

1W个 Client程序,但是只有1个到达了数据,其余的9999次非阻塞的read(要把read套在循环里)都是无效的。

在这里插入图片描述

比方说100个文件描述符(对应100个缓冲区),内核会去看它们谁的读缓冲区有内容了,然后提醒说有几个有内容了,你自己再去遍历看是哪几个有了。(会把标志位设置为1 )

在这里插入图片描述

select

主旨思想:

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O
    操作时,该函数才返回。
    a.这个函数是阻塞
    b.函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
// sizeof(fd_set) = 128bytes 1024bits (1024个标志位的文件描述符)
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
			fd_set *exceptfds, struct timeval *timeout);
	- 参数:
		- nfds : 委托内核检测的最大文件描述符的值 + 1
		- readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
				  - 一般检测读操作
				  - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
				  - 是一个传入传出参数(检测完之后就把0/1修改了)
		- writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
				  - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
            		不满置为1可以写,满了是0不可写
		- exceptfds : 检测发生异常的文件描述符的集合
		- timeout : 设置的超时时间
            struct timeval {
            long tv_sec; /* seconds */
            long tv_usec; /* microseconds */
            };
			- NULL : 永久阻塞,直到检测到了文件描述符有变化
			- tv_sec = 0 tv_usec = 0, 不阻塞
			- tv_sec > 0 tv_usec > 0, 阻塞对应的时间
		- 返回值 :
				- -1 : 失败
				- >0(n) : 检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);

在这里插入图片描述

+1是因为要遍历到它

内核发现 A、B 发送的数据到了,所以把3和4设置为1,但是没检测到100和101的数据,所以把他们置为0了

有一个从用户态拷贝到内核态、再从内核态拷贝到用户态的一个过程

在这里插入图片描述

一开始是有这四个的连接的,所以给他们先在用户态里设置1

在这里插入图片描述

服务器:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main() {

    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    
    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //BIO的效率是非常低的
    fd_set rdset, tmp;  //rdset只允许用FD_SET、FD_CLR、FD_ZERO修改,tmp用内核修改
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;    //一开始就是012、3(监听)

    while(1)
    {
        //rdset 每次只是被我自己修改,它指定了内核需要去检查哪些fd
        //不会在内核里改过改成0了,下次就没法再检查那个fd了,检查哪个由rdset决定
        tmp = rdset;
        //调用select函数,让内核帮检测哪些文件描述符有数据
        //超时时间为NULL表示永久阻塞,直到内核检测出哪些发生了变化才会不阻塞
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1)
        {
            perror("select");
            exit(-1);
        }else if(ret == 0){
            //没有检测到
            continue;
        }else if(ret > 0){
            //说明检测到了有文件描述符对应的缓冲区的数据发生了改变
            //分两步判断
            if(FD_ISSET(lfd, &tmp))
            {
                //有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
                
                //将新的文件描述符加入到fdset
                FD_SET(cfd, &rdset);
                maxfd = maxfd > cfd ? maxfd : cfd;
            }

            //看其他的文件描述符有没有变化
            for(int i = lfd + 1; i <= maxfd; i++)
            {
                if(FD_ISSET(i, &tmp)){
                    //说明文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1)
                    {
                        perror("read");
                        exit(-1);
                    }else if(len == 0){
                        printf("client closed...\n");
                        //断开连接了,不用检查了,把它从检查的列表里拿出来
                        close(i);//记得关掉
                        FD_CLR(i, &rdset);
                    }else if(len > 0){
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    
    close(lfd);
    return 0;
}


客户端:

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "192.168.0.74", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        // sleep(1);
        usleep(1000);
    }

    close(fd);

    return 0;
}

poll

#include <poll.h>
struct pollfd {
	int fd; 			/* 委托内核检测的文件描述符 */
	short events; 		/* 委托内核检测文件描述符的什么事件 */ 要检测的事件
	short revents; 		/* 文件描述符实际发生的事件 */		内核返回的事件
};
// 可以重用、且无1024的限制、内核修改的是revents

struct pollfd myfd;
	myfd.fd = 5;
	myfd.events = POLLIN | POLLOUT;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
	- 参数:
		- fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
		- nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
		- timeout : 阻塞时长
			0 : 不阻塞
		   -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
		   >0 : 阻塞的时长
		- 返回值:
			-1 : 失败
			>0(n) : 成功,n表示检测到集合中有n个文件描述符发生变化

在这里插入图片描述

服务器:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>

int main() {

    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    
    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++)
    {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;

    int nfds = 0;

    while(1)
    {
        
        //调用poll系统函数,让内核帮忙检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        
        if(ret == -1)
        {
            perror("poll");
            exit(-1);
        }else if(ret == 0){
            //没有检测到
            continue;
        }else if(ret > 0){
            //说明检测到了有文件描述符对应的缓冲区的数据发生了改变
            //分两步判断
            if(fds[0].revents & POLLIN)
            {
                //有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
                
                //将新的文件描述符加入到fdset
                for(int i = 1; i < 1024; i++)
                {
                    if(fds[i].fd == -1)
                    {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }
                nfds = nfds > cfd ? nfds : cfd;
            }

            //看其他的文件描述符有没有变化
            for(int i = 1; i <= nfds; i++)
            {
                if(fds[i].revents & POLLIN){
                    //说明文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1)
                    {
                        perror("read");
                        exit(-1);
                    }else if(len == 0){
                        printf("client closed...\n");
                        //断开连接了,不用检查了,把它从检查的列表里拿出来
                        //注意是要先close, 再置为-1
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    }else if(len > 0){
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    
    close(lfd);
    return 0;
}

epoll

epoll的效率要比select和poll高

eventpoll是在内核区中的数据结构,它的返回值是文件描述符,然后我们去操作它

select/poll是要创建一个fd_set或者数组,把它传递给select/poll,然后从用户态拷贝到内核态

这里是直接建立到内核态,且这里的数据结构效率更高,原来是线性的数据结构,这里是红黑树

在这里插入图片描述

之前select/poll是把整个要拷贝回去,但这里rdlist拷贝很快,就只是拷贝就绪的

#include <sys/epoll.h>
// 创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
   - 参数:
   	size : 目前没有意义了。随便写一个数,必须大于0
   - 返回值:
   	-1 : 失败
   	> 0 : 文件描述符,操作epoll实例的

typedef union epoll_data {
   void *ptr;
   int fd;
   uint32_t u32;
   uint64_t u64;
} epoll_data_t;

struct epoll_event {
   uint32_t events; /* Epoll events */
   epoll_data_t data; /* User data variable */
};

常见的Epoll检测事件:
   - EPOLLIN
   - EPOLLOUT
   - EPOLLERR
   
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
   - 参数:
   		- epfd : epoll实例对应的文件描述符
   		- op : 要进行什么操作
   				EPOLL_CTL_ADD: 添加	(添加到红黑树里)
   				EPOLL_CTL_MOD: 修改
   				EPOLL_CTL_DEL: 删除
   		- fd : 要检测的文件描述符
   		- event : 检测文件描述符什么事情(读?写?)
               
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
   - 参数:
   	- epfd : epoll实例对应的文件描述符
   	- events : 传出参数,保存了发送了变化的文件描述符的信息
   	- maxevents : 第二个参数结构体数组的大小
   	- timeout : 阻塞时间
   		- 0 : 不阻塞
   		- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
   		- > 0 : 阻塞的时长(毫秒)
   	- 返回值:
   		- 成功,返回发送变化的文件描述符的个数 > 0
   		- 失败 -1

注意如果上面的事件还包括了EPOLLOUT写,那么下面捕获到其他curfd的时候,如果我们是只是针对读事件,我们还需要单独处理

if(curfd == lfd){
	struct sockaddr_in cliaddr;
	int len = sizeof(cliaddr);
	int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

	epev.events = EPOLLIN | EPOLLOUT;
	epev.data.fd = cfd;
	epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
}
else
{
    if(epevs[i].events & EPOLLOUT)
    {
        ...
    }
}

服务端代码:

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>

int main()
{
    //创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    
    //绑定
    bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));

    //监听
    listen(lfd, 8);

    //调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    //将监听的文件描述符相关的检测信息添加到epoll实例当中(加到rbr里)
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];
    while (1)
    {
        //把已经就绪的、有数据发生改变的文件描述符的信息放在里面
        //1024是前一个参数的大小?
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1)
        {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);
        
        for(int i = 0; i < ret; i++)
        {
            int curfd = epevs[i].data.fd;

            if(curfd == lfd){
                //监听的文件描述符有数据到达,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

                epev.events = EPOLLIN | EPOLLOUT;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            }else{
                //有数据到达了,需要通信 
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1)
                {
                    perror("read");
                    exit(-1);
                }else if(len == 0){
                    printf("client closed...\n");
                    //从红黑树里面删除掉
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                }else if(len > 0){
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
            }
        }
    }
    
    close(lfd);
    close(epfd);

    return 0;
}

EPOLL的两种工作模式

Epoll 的工作模式:

  • LT 模式 (水平触发)(只要缓冲区有数据还没读,就回同意)
    假设委托内核检测读事件 -> 检测fd的读缓冲区
    读缓冲区有数据 - > epoll检测到了会给用户通知
    a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
    b.用户只读了一部分数据,epoll会通知
    c.缓冲区的数据读完了,不通知

LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。

  • ET 模式(边沿触发)共有8个字节,你只读了两个字节,下次就不通知了,你读不到后面的了,所以只能循环地去读,但是如果循环地读,read会阻塞,所以这里用非阻塞套接字接口
    假设委托内核检测读事件 -> 检测fd的读缓冲区
    读缓冲区有数据 - > epoll检测到了会给用户通知
    a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
    b.用户只读了一部分数据,epoll不通知
    c.缓冲区的数据读完了,不通知

ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

比如定义服务器从缓冲区里读的buf大小为5,那输入一个字符串要从缓冲区里读好几次才能读完,对于lt来说,读不完它会一直提醒,一直调用epoll_wait,一直循环

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

EPOLL_ET

struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件:
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET

在哪里设置边沿触发?

epev.events = EPOLLIN | EPOLLET;

这样的话有数据只通知一次

int ret = epoll_wait(epfd, epevs, 1024, -1);
//再次while过来这个函数它是不会通知你的,epvs里是没给你添加内容的
//虽然缓冲区里还有数据,但是就阻塞在这里了

这个时候你再在客户端输入一些内容发送,它这边会继续从缓存里读五个(因为有了新数据进来),有了新的信号,但是仍然读一次读不完,读一次就卡在这了

解决:一次性把数据读出来

注意read阻塞与否是由文件描述符决定

// 设置cfd属性非阻塞
int flag = fcntl(cfd, F_GETFL);
flag | O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

注意上面的服务器的代码里,read完之后再write的数据是回写的数据

有关这里使用循环非阻塞read的注意:

len==-1时不能直接退出,如果是因为读完了等于-1(EAGAIN),还要继续再次循环

else if(len == -1) {
	//注意等于-1时,不能直接退出,等于-1包括一种情况是读完了,但是后面还要继续再读的所以
	if(errno == EAGAIN) {
		printf("data over.....");
	}else {
		perror("read");
		exit(-1);
	}                    
}

整体ET代码(服务端):

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag | O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
                    // 打印数据
                    // printf("recv data : %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
                    printf("client closed....");
                }else if(len == -1) {
                    //注意等于-1时,不能直接退出,等于-1包括一种情况是读完了,但是后面还要继续再读的所以
                    if(errno == EAGAIN) {
                        printf("data over.....");
                    }else {
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

谁先用CTRL C停止,谁要处于TIMEWAIT阶段一分钟

UDP通信

在这里插入图片描述

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
	- 参数:
		- sockfd : 通信的fd
		- buf : 要发送的数据
		- len : 发送数据的长度
		- flags : 0
		- dest_addr : 通信的另外一端的地址信息
		- addrlen : 地址的内存大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
	- 参数:
		- sockfd : 通信的fd
		- buf : 接收数据的数组
		- len : 数组的大小
        - flags : 0
		- src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL
		- addrlen : 地址的内存大小

UDP不需要多线程/进程就可以实现多个客户端访问服务器

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "192.168.0.74", &saddr.sin_addr.s_addr);

    int num = 0;
    // 3.通信
    while(1) {

        // 发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello , i am client %d \n", num++);
        //要给上服务端的地址
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));

        // 接收数据
        int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("server say : %s\n", sendBuf);

        sleep(1);
    }

    close(fd);
    return 0;
}

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    // 2.绑定
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        char recvbuf[128];
        char ipbuf[16];

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);

        // 接收数据
        int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);

        printf("client IP : %s, Port : %d\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
            ntohs(cliaddr.sin_port));

        printf("client say : %s\n", recvbuf);

        // 发送数据
        // cliaddr -> 要给接收端的那个地址
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    }

    close(fd);
    return 0;
}

广播

向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
a.只能在局域网中使用。
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。

在这里插入图片描述

// 设置广播属性的函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
	- sockfd : 文件描述符
	- level : SOL_SOCKET
	- optname : SO_BROADCAST
	- optval : int类型的值,为1表示允许广播
	- optlen : optval的大小

广播的时候,服务端发送数据,客户端需要绑定IP和端口才能接收数据

服务器指定的cliaddr地址是192.168.0.255

客户端是绑定了本地的网卡IP地址

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 2.设置广播属性
    int op = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));
    
    // 3.创建一个广播的地址
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "192.168.0.255", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
       
        char sendBuf[128];
        sprintf(sendBuf, "hello, client....%d\n", num++);
        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        printf("广播的数据:%s\n", sendBuf);
        sleep(1);
    }

    close(fd);
    return 0;
}

客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct in_addr in;

    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say : %s\n", buf);

    }

    close(fd);
    return 0;
}

组播(多播)

单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据

组播地址

IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 ,并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类:

在这里插入图片描述

服务器端设置多播属性,客户端加入到多播组

int setsockopt(int sockfd, int level, int optname,const void *optval,socklen_t optlen);
	// 服务器设置多播的信息,外出接口
	- level : IPPROTO_IP
	- optname : IP_MULTICAST_IF
	- optval : struct in_addr
	// 客户端加入到多播组:
	- level : IPPROTO_IP
	- optname : IP_ADD_MEMBERSHIP
	- optval : struct ip_mreq
        

struct ip_mreq
{
	/* IP multicast address of group. */
	struct in_addr imr_multiaddr; // 组播的IP地址
	/* Local IP address of interface. */
	struct in_addr imr_interface; // 本地的IP地址
};

typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

本地套接字

本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。


// 本地套接字通信的流程 - tcp
// 服务器端
1. 创建监听的套接字
	int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的套接字文件 -> server端
	struct sockaddr_un addr;
	// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
	bind(lfd, addr, len);
3. 监听
	listen(lfd, 100);
4. 等待并接受连接请求
	struct sockaddr_un cliaddr;
	int cfd = accept(lfd, &cliaddr, len);
5. 通信
	接收数据:read/recv
	发送数据:write/send
6. 关闭连接
	close();

// 客户端的流程
1. 创建通信的套接字
	int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0);
2. 监听的套接字绑定本地的IP 端口
	struct sockaddr_un addr;
	// 绑定成功之后,指定的sun_path中的套接字文件会自动生成。
	bind(lfd, addr, len);
3. 连接服务器
	struct sockaddr_un serveraddr;
	connect(fd, &serveraddr, sizeof(serveraddr));
4. 通信
	接收数据:read/recv
	发送数据:write/send
5. 关闭连接
	close();

客户端和服务器绑定的套接字文件是伪文件,真正数据还是在内存缓冲区中

在这里插入图片描述

写的话是写在自己的写缓冲区里,然后它就到了对方的读里面,对方去读

在用bind绑定地址的时候绑定的是本地sockaddr_un的文件,它里面的sun_path是".sock"文件,会新建

客户端和服务器都要用bind绑定本地套接字文件(代替绑定IP、Port)

服务器

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {

    unlink("server.sock");

    // 1.创建监听的套接字
    int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "server.sock");
    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 100);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.等待客户端连接
    struct sockaddr_un cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    printf("client socket filename: %s\n", cliaddr.sun_path);

    // 5.通信
    while(1) {

        char buf[128];
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        } else if(len > 0) {
            printf("client say : %s\n", buf);
            send(cfd, buf, len, 0);
        }

    }

    close(cfd);
    close(lfd);

    return 0;
}

客户端

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {

    unlink("client.sock");

    // 1.创建套接字
    int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(cfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock");
    int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.连接服务器
    struct sockaddr_un seraddr;
    seraddr.sun_family = AF_LOCAL;
    strcpy(seraddr.sun_path, "server.sock");
    ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 4.通信
    int num = 0;
    while(1) {

        // 发送数据
        char buf[128];
        sprintf(buf, "hello, i am client %d\n", num++);
        send(cfd, buf, strlen(buf) + 1, 0);
        printf("client say : %s\n", buf);

        // 接收数据
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("server closed....\n");
            break;
        } else if(len > 0) {
            printf("server say : %s\n", buf);
        }

        sleep(1);

    }

    close(cfd);
    return 0;
}

对于网络通信bind的理解:谁是一开始接收数据的那一方,谁就bind,那样才能第一次找到他。对于本地的通信,两个都要绑定本地socket文件

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值