依然数据,进程间通信的范畴。 网络通信是不同主机间的进程间通信。

1、OSI 模型 ===》开放系统互联模型 ==》分为7层: open system
interconnect

应用层:为网络用户提供各种服务,例如电子邮件、文件传输等。
表示层:为不同主机间的通信提供统一的数据表示形式。
会话层:负责信息传输的组织和协调,管理进程会话过程。
传输层:管理网络通信两端的数据传输,提供可靠或不可靠的传输服务。
网络层:负责数据传输的路由选择和网际互连。
数据链路层,负责物理相邻(通过网络介质相连)的主机间的数据传输,主要作用包括物理地址寻址、
数据帧封装、差错控制等。该层可分为逻辑链路控制子层(LLC)和介质访问控制子层(MAC)。
物理层,负责把主机中的数据转换成电信号,再通过网络介质(双绞线、光纤、无线信道等)来传输。
该层描述了信设备的机械、电气、功能等特性。

2.tcp/ip 模型

tcp/ip模型,是从osi 模型简化而来。

tcp/ip协议栈
应用层 ====》应用程序
传输层 ====》端口号tcp udp
网络层 ====》IP 地址
接口层 ====》网卡 驱动 1GB


3.常用网络协议

应用层: HTTP TFTP FTP SNMP DNS ...
传输层: TCP UDP tcp 传输控制协议 udp 用户数据报协议
网络层: IP ICMP(ping) RIP OSPF IGMP ...
接口层 : ARP RARP ... ip--->mac 地址解析

4.ip组成
IP地址 == 网络位 + 主机位
IP地址的分类: 点分十进制 ipv4 712934
A类: 超大规模性网络
8 8 8 8
1.0.0.0 - 126.255.255.255 126.1.1.1
126.1.1.2
255.0.0.0
私有:
10.0.0.0 - 10.255.255.255
127.0.0.1
B类: 大中规模型网络
128.0.0.0 - 191.255.255.255
128.2.1.2 128.2.7.2
255.255.0.0
私有:
172.16.0.0 - 172.31.255.255
5. 网络常用命令
C类: 中小规模型网络
192.0.0.0 - 223.255.255.255
255.255.255.0
私有:
192.168.0.0 - 192.168.255.255
静态路由
192.168.0.0
192.168.0.1 网关
192.168.0.255
D类: 组播和广播
224.0.0.0 - 239.255.255.255
192.168.0.255 == 255.255.255.255
235.1.2.3
192.168.1.0
192.168.0.1 网关
192.168.1.255 广播
E类: 实验
240.0.0.0 - 255.255.255.255
1.
ping 用于网络通断检测
ping www.baidu.com
2.
ifconfig
查看网络适配器的状态信息 查看自己的ip
3
netstat -anp
查看本机 所有的网络连接的状态
 

6. 名字解释

1、
socket 套接字 ,网络设备对应的文件描述符。
socket API application interface 用于网络通信的一组接口函数
2、ip+port
地址+端口===》地址用来识别主机
端口用来识别应用程序
port分为TCP port / UDP port 范围都是: 1-65535
3、
网络字节序 网络设备都是大端存储
主机字节序 都是小端存储

7.udp 用户数据报
1.特征
数据报
无连接 不可靠 网络延迟低 开销小
1.发送次数和接收次数要对应
2.发送和接收大小 ,保持一致。如果接收的大小小,剩下的数据就接收不到了(1包数据)。
3.每次发送数据,链路都是不同的。
4.对于udp而言,是有读阻塞。
5.没有写阻塞。发送方发送数据太快,接收发没有及时接收,就会出现丢包。
6.半双工

2.编程步骤

3.相关函数

int socket(int domain, int type, int protocol);
功能:程序向内核提出创建一个基于内存的套接字描述符
参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
                        PF_UNIX == AF_UNIX ==>单机程序
    type 套接字类型:
            SOCK_STREAM 流式套接字 ===》TCP
            SOCK_DGRAM 用户数据报套接字===>UDP
            SOCK_RAW 原始套接字 ===》IP
    protocol 协议 ==》0 表示自动适应应用层协议。

返回值: 成功 返回申请的套接字id
        失败 -1;

2、int bind(int sockfd, struct sockaddr *my_addr,socklen_t addrlen);
功能:如果该函数在服务器端调用,则表示将参数1相关
的文件描述符文件与参数2 指定的接口地址关联,
用于从该接口接受数据。

如果该函数在客户端调用,则表示要将数据从
参数1所在的描述符中取出并从参数2所在的接口
设备上发送出去。

注意:如果是客户端,则该函数可以省略,由默认
接口发送数据。

参数:sockfd 之前通过socket函数创建的文件描述符,套接字id
      my_addr 是物理接口的结构体指针。表示该接口的信息。

struct sockaddr ////通用地址结构
{
    u_short sa_family; ////地址族
    char sa_data[14]; ////地址信息
};

转换成网络地址结构如下:
struct _sockaddr_in ///网络地址结构
{
    u_short sin_family; ////地址族
    u_short sin_port; ///地址端口
    struct in_addr sin_addr; ///地址IP
    char sin_zero[8]; ////占位
};

struct in_addr
{
    in_addr_t s_addr;
}
socklen_t addrlen: 参数2 的长度。

返回值:成功 0
       失败 -1;


发送接收函数:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
功能:用于UDP协议中向对方发送数据。
参数:sockfd 本地的套接字id
      buff 本地的数据存储,一般是要发送的数据。
      len 要发送的数据长度
      flags 要发送数据方式,0 表示阻塞发送。
      dest_addr: 必选,表示要发送到的目标主机信息结构体。
      addrlen :目标地址长度。

返回值:成功 发送的数据长度
       失败 -1;


ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
功能:用于UDP协议中获取对方发送的数据。
参数:sockfd 本地的套接字id
      buff 要存储数据的内存区,一般是数组或者动态内存。
      len 要获取的数据长度,一般是buff的大小。
      flags 获取方式,0 阻塞
      src_addr 可选,表示对方的地址信息结构体,

下面是使用udp实现的服务端和客户端示例

server端:

// server 服务端
#include <bits/types/time_t.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr* (SA);
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET,SOCK_DGRAM,0);
    if (udpfd == -1)
    {
        perror("socket");
        return 1;
    }
    // 2. 给套接字 绑定ip ,端口号
    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser, cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    //指定网络类型
    ser.sin_family = AF_INET; // ipv4 
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000); // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("192.168.31.125");
    //套接字绑定地址结构体
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);
    while (1)
    {
        char buf[1024] = {0};
        //先从客户端收消息
        recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
        time_t tm;
        time(&tm);
        sprintf(buf, "%s %s", buf,ctime(&tm));
        //将信息写入,并发回客户端
        sendto(udpfd, buf, sizeof(buf), 0, (SA)&cli, len);
    }
    
    return 0;
}

client端:

// client 客户端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    //指定网络类型
    ser.sin_family = AF_INET;     // ipv4
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("192.168.31.177");

    while (1)
    {
        char buf[1024] = "hello,this is tcp test";
        sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
        bzero(buf, sizeof(buf));
        //后两个参数为NULL是因为这是客户端,服务器对于客户端来说是唯一的
        //所以不用指定从哪里接受信息
        recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
        printf("from ser:%s\n", buf);
        sleep(1);
    }

    close(udpfd);
    return 0;
}

下面用udp实现文件的发送

server端发文件:c

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
//通用地址结构体
typedef struct sockaddr* (SA);
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser,cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    //指定网络类型
    ser.sin_family = AF_INET; // ipv4 
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000); // host to net short 小端转大端
    //初始化ip地址 //INADDR_ANY 表示本机地址 = 本机真是ip  一般写服务端
    ser.sin_addr.s_addr = INADDR_ANY;
    //套接字绑定地址结构体
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);
    char buf[1024] = {0};
    
    //先从客户端收消息
    recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
    printf("%s\n",buf);
    int fd = open("1.jpeg", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    char s[1024] = {0};

    while (1)
    {
        bzero(s, sizeof(s));
        ret = read(fd, s, sizeof(s));
        if (ret == 0)
        {
            printf("文件发完了\n");
            break;
        }
        sendto(udpfd, s, ret, 0, (SA)&cli, len);
        bzero(s, sizeof(s));
        //由于该程序是在本机间通信,读的速度大于写的速度,
        //所以为了读写速度一致(读的次数等于写的次数),recvform能够在这阻塞,直到接收端发来信息再继续下一步的读文件
        recvfrom(udpfd, s, ret, 0, (SA)&cli, &len);
        //usleep(1000*10);
    }
    //由于接收段recvfrom在socket中没有内容时会阻塞,所以需要给接收段传信息,通知接收段结束
    strcpy(s,"aaa");
    sendto(udpfd, s, strlen(s), 0, (SA)&cli, len);

    close(udpfd);
    close(fd);
    return 0;
}

client端收文件:

#include <fcntl.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/stat.h>
#include <string.h>
typedef struct sockaddr* (SA);
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    //指定网络类型
    ser.sin_family = AF_INET;     // ipv4
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("192.168.31.125");
    
    
    char s[1024] = "请发文件";
    sendto(udpfd, s, strlen(s), 0, (SA)&ser, sizeof(ser));
    int fd = open("5.jpeg", O_WRONLY |O_CREAT|O_TRUNC,0666);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    
    while (1)
    {
        char buf[1024] = {0};
        int ret = recvfrom(udpfd, buf, sizeof(buf), 0, NULL, NULL);
        //buf[ret] = '\0';
        if (strcmp(buf, "aaa") == 0)//不执行的原因:recvfrom读取后没有\0
        {
            printf("%s\n",buf);
            break;
        }
        
        write(fd, buf, ret);
        bzero(buf, sizeof(buf));
        strcpy(buf, "go on");
        sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
        //bzero(buf, sizeof(buf));
    }
    close(fd);
    close(udpfd);
    return 0;
}

我们可以使用udp实现一个聊天室,因为收发消息是并发的,所以这里我们在客户端和服务端都创建两个线程,一个负责收消息,一个负责发消息。

a:

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

typedef struct sockaddr* (SA);

typedef struct ip_info{
    int fd;
    struct sockaddr *addr;
    socklen_t addr_len;
}info;
//接受信息
void* func1(void* arg){
    struct ip_info ip = *(struct ip_info*)arg;
    while (1)
    {
        char buf[1024] = {0};
        recvfrom(ip.fd, buf, sizeof(buf), 0, ip.addr, &(ip.addr_len));
        if (strcmp(buf, "quit\n") == 0)
        {
            printf("退出\n");
            exit(0);
        }
        printf("from B:%s\n",buf);
    }
    return NULL;
}
//发送信息
void* func2(void* arg){
    struct ip_info ip = *(struct ip_info*)arg;
    while (1)
    {
        char buf[1024] = {0};
        printf("to B\n");
        fgets(buf, sizeof(buf), stdin);
        sendto(ip.fd, buf, sizeof(buf), 0, ip.addr, ip.addr_len);
        if (strcmp(buf, "quit\n") == 0)
        {
            printf("退出\n");
            exit(0);
        }
    }
    return NULL;
}
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET,SOCK_DGRAM,0);
    if (udpfd == -1)
    {
        perror("socket");
        return 1;
    }
    // 2. 给套接字 绑定ip ,端口号
    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser, cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    //指定网络类型
    ser.sin_family = AF_INET; // ipv4 
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000); // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("192.168.31.125");
    //套接字绑定地址结构体 IP地址和端口号
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);
    pthread_t tid1;
    pthread_t tid2;

    //初始化ip结构体
    struct ip_info ip;
    ip.fd = udpfd;
    ip.addr = (SA)&cli;
    ip.addr_len = len;

    char buf[1024] = {0};
    //得到客户端的 地址
    recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
    printf("%s\n",buf);

    pthread_create(&tid1, NULL, func1, &ip);
    pthread_create(&tid2, NULL, func2, &ip);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

b端:

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

typedef struct sockaddr *(SA);

typedef struct ip_info{
    int fd;
    struct sockaddr *addr;
    socklen_t addr_len;
}info;
//接受信息
void* func1(void* arg){
    struct ip_info ip = *(struct ip_info*)arg;
    while (1)
    {
        char buf[1024] = {0};
        recvfrom(ip.fd, buf, sizeof(buf), 0, ip.addr, &(ip.addr_len));
         if (strcmp(buf, "quit\n") == 0)
        {
            printf("退出\n");
            exit(0);
        }
        printf("from A:%s\n",buf);
    }
    return NULL;
}
//发送信息
void* func2(void* arg){
    struct ip_info ip = *(struct ip_info*)arg;
    while (1)
    {
        char buf[1024] = {0};
        printf("to A\n");
        fgets(buf, sizeof(buf), stdin);
        sendto(ip.fd, buf, sizeof(buf), 0, ip.addr, ip.addr_len);
         if (strcmp(buf, "quit\n") == 0)
        {
            printf("退出\n");
            exit(0);
        }
    }
    return NULL;
}
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    //指定网络类型
    ser.sin_family = AF_INET;     // ipv4
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("192.168.31.125");

    socklen_t len = sizeof(ser);
    pthread_t tid1;
    pthread_t tid2;

    //初始化ip结构体
    struct ip_info ip;
    ip.fd = udpfd;
    ip.addr = (SA)&ser;
    ip.addr_len = len;

    char buf[1024] = "咱们开始聊天吧";
    //告知服务端自己的 地址
    sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
    
    pthread_create(&tid1, NULL, func1, &ip);
    pthread_create(&tid2, NULL, func2, &ip);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    close(udpfd);
    return 0;
}

TCP 传输控制协议
1. 特征
有链接 ,在正式通信之前,需要先确定一条链路。在通信过程中,需要一致保持。直到其中
一方需要断开为止。
可靠的传输。在发送数据过程,数据都能到达对发。如果出现错误,可以收发函数的返回值判
断。
应答机制,超时重传机制。拥塞控制。
因为需要可靠传输,就需要维护链路的状态信息,网络开销,网络延迟大
全双工 ,同一时刻,可以同时收发
流式套接字
2. 编程步骤
如果为NULL,表示不关心对方地址。
addrlen 对方地址信息结构体大小。
如果对方地址是NULL,则该值也为NULL。
返回值:成功 接收到的数据长度
失败 -1;
流式套接字
1 ,发送的次数和接收的次不需要对应
2 , 数据与数据是连续。
3. 顺序(tcp), udp不保证顺序到达
4.有读阻塞,写阻塞
64K

3.三次握手,四次挥手过程

4.黏包问题
发送方多次发送数据,
接收方无法正常解析数据。
解决方案:
设置结束标志
发送固定大小,struct
自定义协议 ,变长

5.通信套接字和监听套接字
客户端刚开始连接服务器的,先找listfd(监听套接字,IP+port) .一旦成功建立连接,服务端会产生一个
新的套接字conn(通信套接字)。对于服务端而言,如果需要给第一个客户端通信的话,给 下图的4 发数据...

6.相关函数

int socket(int domain, int type, int protocol);
功能:程序向内核提出创建一个基于内存的套接字描述符

参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
                    PF_UNIX == AF_UNIX ==>单机程序
      type 套接字类型:SOCK_STREAM 流式套接字 ===》TCP
                      SOCK_DGRAM 用户数据报套接字===>UDP
                      SOCK_RAW 原始套接字 ===》IP
protocol 协议 ==》0 表示自动适应应用层协议。

返回值:成功 返回申请的套接字id
        失败 -1;


2、int bind(int sockfd, struct sockaddr *my_addr,
socklen_t addrlen);
功能:如果该函数在服务器端调用,则表示将参数1相关
的文件描述符文件与参数2 指定的接口地址关联,
用于从该接口接受数据。
如果该函数在客户端调用,则表示要将数据从
参数1所在的描述符中取出并从参数2所在的接口
设备上发送出去。

3. int listen(int sockfd, int backlog);
功能:在参数1所在的套接字id上监听等待链接。
参数:sockfd 套接字id
      backlog 允许链接的个数。
返回值:成功 0
       失败 -1

4、int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
功能:从已经监听到的队列中取出有效的客户端链接并
接入到当前程序。

参数:sockfd 套接字id
      addr 如果该值为NULL ,表示不论客户端是谁都接入。
            如果要获取客户端信息,则事先定义变量
           并传入变量地址,函数执行完毕将会将客户端
           信息存储到该变量中。
      addrlen: 参数2的长度,如果参数2为NULL,则该值也为NULL;
                如果参数不是NULL,&len;
                一定要写成len = sizeof(struct sockaddr);
返回值:成功 返回一个用于通信的新套接字id;
            从该代码之后所有通信都基于该id
        失败 -1;

5.ssize_t recv(int sockfd, void *buf, size_t len,
int flags);
功能:从指定的sockfd套接字中以flags方式获取长度
为len字节的数据到指定的buff内存中。

参数:sockfd
        如果服务器则是accept的返回值的新fd
        如果客户端则是socket的返回值旧fd
buff 用来存储数据的本地内存,一般是数组或者
    动态内存。
len 要获取的数据长度
flags 获取数据的方式,0 表示阻塞接受。

返回值:成功 表示接受的数据长度,一般小于等于len
        失败 -1;

6. int send(int sockfd, const void *msg,
size_t len, int flags);
功能:从msg所在的内存中获取长度为len的数据以flags
        方式写入到sockfd对应的套接字中。
参数:sockfd:
        如果是服务器则是accept的返回值新fd
        如果是客户端则是sockfd的返回值旧fd

7. int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:该函数固定有客户端使用,表示从当前主机向目标
主机发起链接请求。

参数:sockfd 本地socket创建的套接子id
     addr 远程目标主机的地址信息。
     addrlen: 参数2的长度。

返回值:成功 0
       失败 -1;

数据的封包拆包

协议头
mac 头
以太网V2 MAC帧

ip头 大小:20byte
ttl 生命周期 ,是一个整形的数字(一般初值64)。 每过一个网络节点,就减1
ip flag ,D,是否允许分片 M,是否是最后一片。

tcp 头 大小:20byte
tcp flag , u 表示本帧数据中有紧急数据
a,表示本帧数据中有应答数据
p,表示本帧数据中有应用层数据的传输
r,表示本帧数据中有reset 操作
s,表示本帧数据中有三次握手
f,表示本帧数据中有四次挥手


udp 头 大小:8byte

我们可以写一个TCP的小示例

server端:

#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    // 1 打开网络设备,获得文件描述(套接字)
    // 监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listfd == -1)
    {
        perror("listfd");
        return 1;
    }

    struct sockaddr_in ser,cli;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    // 2 给套接字绑定 ip +port ,方便客户端找到服务器
    int ret = bind(listfd, (SA)&ser,sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }

    // 3 进入监听状态 (套接字进入可以被链接的状态)
    // 参数2  ,进行三次握手的排队数
    listen(listfd,3);

    socklen_t len = sizeof(cli);

    // 4 建立链接  (触发三次握手)
    // 通信套接字,表示从服务端来看,conn 是客户端的套接字
    int conn = accept(listfd, (SA)&cli,&len);
    if (conn == -1)
    {
        perror("accept");
        return 1;
    }

    while (1)
    {
        char buf[1024] = {0};
        // 5 接收数据
        // 返回值  >0 实际收到的字节数  ==0  对方断开 -1 错误
        int rd_ret = recv(conn, buf, sizeof(buf), 0);
        if (rd_ret <= 0)
        {
            perror("recv");
            break;
        }
        printf("from client:%s\n",buf);
        time_t tm;
        time(&tm);
        sprintf(buf, "%s %s", buf,ctime(&tm));
        // 6 发送数据
        send(conn, buf, strlen(buf), 0);
    }

    close(listfd);
    close(conn);
    return 0;
}

client端:

#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");
    

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }


    while(1){
        char buf[1024] = "hello, this is a tcp test";
        send(sockfd, buf, strlen(buf), 0);
        bzero(buf, sizeof(buf));
        int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
        if (rd_ret <= 0)
        {
            break;
        }
        printf("from server:%s\n",buf);
        sleep(1);
    }
    close(sockfd);
    return 0;
}

我们使用TCP同样可以实现文件发送

server端发文件:

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listfd == -1)
    {
        perror("listfd");
        return 1;
    }

    struct sockaddr_in ser,cli;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }

    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    int conn = accept(listfd, (SA)&cli,&len);
    if (conn == -1)
    {
        perror("conn");
        return 1;
    }

    int fd = open("1.jpeg", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    while (1)
    {
        char buf[1024] = {0};
        int rd_ret = read(fd, buf, sizeof(buf));
        if (rd_ret <= 0)
        {
            break;
        }
        send(conn, buf, rd_ret, 0);
        bzero(buf, sizeof(buf));
        //会阻塞,可以等待接收端写入文件
        recv(conn, buf, sizeof(buf), 0);
    }
    close(listfd);
    close(conn);
    close(fd);
    return 0;
}

client端:

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }

    int fd = open("3.jpeg", O_WRONLY|O_CREAT|O_TRUNC,0666);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }
    while(1){
        char buf[1024] = {0};
        int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
        if (rd_ret <= 0)//此处为退出方式,当对方断开链接,收发函数的返回值改变,可以判断,程序退出
        {
            break;
        }
        write(fd, buf, rd_ret);
        strcpy(buf, "continue");
        //发送消息给服务端,告诉服务端继续发文件
        send(sockfd, buf, strlen(buf), 0);
    }
    close(sockfd);
    return 0;
}

我们也可以使用TCP实现一个聊天室

a端:

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
//发
void* th1(void* arg){
    while (1)
    {
        int fd = *(int*)arg;
        char buf[1024] = {0};
        printf("to B:");
        fflush(stdout);
        fgets(buf, sizeof(buf), stdin);
        send(fd, buf, strlen(buf), 0);
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(fd);
            exit(0);
        }
    }
    return NULL;
}
//收
void* th2(void* arg){
    while (1)
    {
        int fd = *(int*)arg;
        char buf[1024] = {0};
        //printf("from B:\n");
        recv(fd, buf, sizeof(buf), 0);
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(fd);
            exit(0);
        }
        printf("from B:%s\n",buf);
    }
    return NULL;
}

int	main(int argc, char **argv)
{
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listfd == -1)
    {
        perror("listfd");
        return 1;
    }

    struct sockaddr_in ser,cli;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }

    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    int conn = accept(listfd, (SA)&cli,&len);
    if (conn == -1)
    {
        perror("conn");
        return 1;
    }

    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL,th1, &conn);
    pthread_create(&tid2, NULL,th2, &conn);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

b端:

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
//发
void* th1(void* arg){
    while (1)
    {
        int fd = *(int*)arg;
        char buf[1024] = {0};
        printf("to A:");
        fflush(stdout);
        fgets(buf, sizeof(buf), stdin);
        send(fd, buf, strlen(buf), 0);
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(fd);
            exit(0);
        }
    }
    return NULL;
}
//收
void* th2(void* arg){
    while (1)
    {
        int fd = *(int*)arg;
        char buf[1024] = {0};
        //printf("from A:\n");
        recv(fd, buf, sizeof(buf), 0);
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(fd);
            exit(0);
        }
        printf("from A:%s\n",buf);
    }
    return NULL;
}
int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");
    

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }

    pthread_t tid1,tid2;
    pthread_create(&tid1, NULL,th1, &sockfd);
    pthread_create(&tid2, NULL,th2, &sockfd);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    return 0;
}

与UDP实现的思路一样,由于收发消息是并发执行的,所以这里定义两个线程一个负责收,一个负责发。

TCP模拟查字典功能,客户端向服务端发送单词,服务端负责查单词的意思并发送给客户端打印。

server端

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listfd == -1)
    {
        perror("listfd");
        return 1;
    }

    struct sockaddr_in ser,cli;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;

    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }

    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    int conn = accept(listfd, (SA)&cli,&len);
    if (conn == -1)
    {
        perror("conn");
        return 1;
    }

    int fd = open("dict.txt", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        return 1;
    }

    FILE* fp = fdopen(fd, "r");
    if (fp == NULL)
    {
        perror("fdopen");
        return 1;
    }
    while (1)
    {
        int flag = 0;
        char buf[1024] = {0};
        int ret = recv(conn, buf, sizeof(buf), 0);
        buf[ret-1] = '\0';
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(conn);
            close(fd);
            close(listfd);
            fclose(fp);
            exit(0);
        }
        char s[1024]= {0};
        rewind(fp);
        while (fgets(s, sizeof(s), fp) != NULL)
        {
            int i = 0;
            char* dic[1024];
            dic[0] = strtok(s," ");
            dic[1] = strtok(NULL,"\n");
            if (strcmp(buf, dic[0]) == 0)
            {
                send(conn, dic[1], strlen(dic[1]), 0);
                flag = 1;
                break;
            }
        }
        if (flag == 0)
        {
            char message[1024] = "没找到";
            send(conn, message, strlen(message), 0);
        } 
    }
    close(conn);
    close(listfd);
    return 0;
}

client端:

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }

    while (1)
    {
        char buf[1024] = {0};
        printf("请输出:\n");
        fgets(buf, sizeof(buf), stdin);
        send(sockfd, buf, strlen(buf), 0);
        if (strcmp(buf, "#quit\n") == 0)
        {
            close(sockfd);
            exit(0);
        }
        bzero(buf, sizeof(buf));
        recv(sockfd, buf, sizeof(buf), 0);
        printf("%s\n", buf);
    }
    return 0;
}

wireshark
网络抓包工具,网络调试,协议分析。可以看到网络设备,底层收发到的字节。
1.sudo wireshark
2.选择网络设备
any ,获取所有网络设备的数据(默认)
lo: 本地回环, 获得本机发送或接收的数据
ens33 ,获得通过外网收发的数据

3.设置过滤条件
通过 端口设备 过滤条件 tcp.port == 80 || udp.port == 80
通过ip地址 设置过滤条件 ip.addr == 192.0.2.1
条件之间可以使用 &&(同时满足) ||(满足一个)
界面介绍
最上面的窗口 wireshark 获得的 报文
中间窗口 是tcp/ip 四层模型
最下面窗口 一帧数据对应的字节

http 超文本传输协议 (网页)
是一个应用层的协议。主要用于浏览器和服务器之间的交互。
url , 统一资源定位符。在整个互联网唯一表示一个资源(网页,照片,电影。。。)
http 超文本传输协议, 是基于tcp的。 默认端口号 80 。https(加密版本的http) ,443
html 超文本标记语言, 编写网页的一类编程语言。 经过浏览器的解析,最终显示在浏览器中。
http的交互过程
4 步。
建立TCP连接
发送http 请求报文
接收http响应报文和数据
断开连接

http的请求报文的格式

GET / HTTP/1.1\r\n 请求行
期望接收的数据的类型
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,imag
e/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n
期望接收数据的编码
Accept-Encoding: gzip, deflate\r\n
期望接收数据语言
Accept-Language: zh-CN,zh;q=0.9
连接方式 : 长连接
Connection: keep-alive
请求的主机
Host: news.sohu.com
用户代理
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/145.0.0.0 Safari/537.36\r\n\r\n
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。 io事件(读写)的通知机制。
IO模型
1、阻塞IO 默认
2、非阻塞IO EAGAIN 忙等待 errno 需要掌握设置方法 fctnl
3、信号驱动IO SIGIO 用的相对少(了解)
4、并行模型 进程,线程
5, IO多路复用 select、poll、epoll

下面是用TCP实现http的示例,实现了城市天气预报的功能。通过TCP模拟浏览器向服务器发送http请求,即将http请求头通过TCP的方式放松给服务器。每次将城市名字拼接进请求头中。

#include <fcntl.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include<ctype.h>
typedef struct sockaddr *(SA);

char city[10] = "成都";

char* city_encode(const char* city){
    size_t len = strlen(city);
    // 最坏情况:每个字节都可能变成 %XX(3 个字符),加上结尾的 '\0'
    char *encoded = malloc(len * 3 + 1);
    if (encoded == NULL) {
        return NULL;   // 内存分配失败
    }

    const char *src = city;      // 源指针
    char *dst = encoded;        // 目标指针
    while (*src != '\0') {
        unsigned char c = (unsigned char)(*src); // 转为无符号,避免 isalnum 处理负值

        // 其他字符(包括中文的每个字节)编码为 %XX
        sprintf(dst, "%%%02X", c); // 写入 "%" + 两位大写十六进制
        dst += 3;                  // 跳过刚写入的 3 个字符
        src++;
    }

    *dst = '\0';   // 添加字符串结束符
    return encoded;
}

void set_city(){
    scanf("%s",city);
}

void weather_live(int sockfd,const char* encode){
    char* buff[10] = {NULL};
    char request[1024] = {0};
    char* head = "GET /?app=weather.today&cityNm=";
    char* tale = "&appkey=78670&sign=7859c8f61b3079f734671b484cd60b04&format=json HTTP/1.1\r\n";
    sprintf(request,"%s%s%s", head,encode,tale);
    buff[0] = request;

    buff[1] = "Host: api.k780.com\r\n";
    buff[2] = "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0\r\n";
    buff[3] = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n";
    buff[4] = "Accept-Language: en-US,en;q=0.5\r\n";
    buff[5] = "Accept-Encoding: gzip, deflate\r\n";
    buff[6] = "Connection: keep-alive\r\n";
    buff[7] = "Upgrade-Insecure-Requests: 1\r\n\r\n";
    char buf[2048] = {0};
    for (int i = 0; i < 8; i++)
    {
       send(sockfd, buff[i], strlen(buff[i]), 0);
    }
    int ret = recv(sockfd, buf, sizeof(buf), 0);
    //printf("%s\n",buf);
    
    char *days = strstr(buf, "days");
    char *week = strstr(days, "week");
    char *citynm = strstr(week, "citynm");
    char *temp = strstr(citynm, "temperature");
    char *wea = strstr(temp, "weather");

    char *end = 0;
    days += 7;
    end = strchr(days, '"');
    *end = '\0';

    week += 7;
    end = strchr(week, '"');
    *end = '\0';

    citynm+=9;
     end = strchr(citynm, '"');
    *end = '\0';

    temp+=14;
     end = strchr(temp, '"');
    *end = '\0';

    wea+=10;
     end = strchr(wea, '"');
    *end = '\0';

    printf("%s:%s:%s:%s:%s\n",days,week,citynm,temp,wea);
}

void life_index(int sockfd,const char* encode){
    char* buff[10] = {NULL};

    char request[1024] = {0};
    char* head = "GET /?app=weather.lifeindex&cityNm=";
    char* tale = "&appkey=78670&sign=7859c8f61b3079f734671b484cd60b04&format=json HTTP/1.1\r\n";
    sprintf(request,"%s%s%s", head,encode,tale);
    buff[0] = request;

    buff[1] = "Host: api.k780.com\r\n";
    buff[2] = "User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0\r\n";
    buff[3] = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n";
    buff[4] = "Accept-Language: en-US,en;q=0.5\r\n";
    buff[5] = "Accept-Encoding: gzip, deflate\r\n";
    buff[6] = "Connection: keep-alive\r\n";
    buff[7] = "Upgrade-Insecure-Requests: 1\r\n\r\n";
    char buf[5120] = {0};
    int i = 0;
    for (i = 0; i < 8; i++)
    {
       send(sockfd, buff[i], strlen(buff[i]), 0);
    }
    int ret = recv(sockfd, buf, sizeof(buf), 0);
    char *p = buf;
    for (i = 0; i < 7; i++)
    {
        char *days = strstr(p, "days");
        char *week = strstr(days, "week_1");
        char *citynm = strstr(week, "citynm");
        char *lifeindex_uv_typenm = strstr(citynm, "lifeindex_uv_typenm");
        char *lifeindex_uv_attr = strstr(lifeindex_uv_typenm, "lifeindex_uv_attr");
        char *lifeindex_uv_dese = strstr(lifeindex_uv_attr, "lifeindex_uv_dese");
        char *lifeindex_xc_typenm = strstr(lifeindex_uv_dese, "lifeindex_xc_typenm");
        char *lifeindex_xc_dese = strstr(lifeindex_xc_typenm, "lifeindex_xc_dese");

        char *end = 0;
        days += 7;
        end = strchr(days, '"');
        *end = '\0';

        week += 9;
        end = strchr(week, '"');
        *end = '\0';

        citynm+=9;
        end = strchr(citynm, '"');
        *end = '\0';

        lifeindex_uv_typenm+=22;
        end = strchr(lifeindex_uv_typenm, '"');
        *end = '\0';

        lifeindex_uv_attr+=20;
        end = strchr(lifeindex_uv_attr, '"');
        *end = '\0';

        lifeindex_uv_dese+=20;
        end = strchr(lifeindex_uv_dese, '"');
        *end = '\0';
        
        lifeindex_xc_typenm+=22;
        end = strchr(lifeindex_xc_typenm, '"');
        *end = '\0';

        lifeindex_xc_dese+=20;
        end = strchr(lifeindex_xc_dese, '"');
        *end = '\0';
        printf("%s:%s:%s %s:%s %s:%s:%s\n",days,week,citynm,lifeindex_uv_typenm,lifeindex_uv_attr,lifeindex_uv_dese,lifeindex_xc_typenm,lifeindex_xc_dese);
        p = end + 1;
    }
}

int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }
  
    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(80);
    ser.sin_addr.s_addr = inet_addr("8.129.233.227");

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }
    //printf("%s\n",city_encode("成都"));
    while (1)
    {
        printf("1 设置城市\n");
        printf("2 实时天气\n");
        printf("3 生活指数\n");
        printf("4 退出\n");
        int choice;
        scanf("%d",&choice);
        switch (choice)
        {
            case 1:
                set_city();
                break;
            case 2:
                weather_live(sockfd,city_encode(city));
                break;
            case 3:
                life_index(sockfd,city_encode(city));
                break;
            case 4:
                exit(0);
                break;
        default:
            break;

        }
    }

    close(sockfd);

    return 0;
}

多路io
IO多路复用
作用:
应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。
单个执行体(单进程,单线程)中,需要检测多个阻塞设备。
select 使用的步骤
GET / HTTP/1.1\r\n 请求行
期望接收的数据的类型
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,imag
e/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n
期望接收数据的编码
Accept-Encoding: gzip, deflate\r\n
期望接收数据语言
Accept-Language: zh-CN,zh;q=0.9
连接方式 : 长连接
Connection: keep-alive
请求的主机
Host: news.sohu.com
用户代理
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/145.0.0.0 Safari/537.36\r\n\r\n
定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。 io事件(读写)的通知机制。
IO模型
1、阻塞IO 默认
2、非阻塞IO EAGAIN 忙等待 errno 需要掌握设置方法 fctnl
3、信号驱动IO SIGIO 用的相对少(了解)
4、并行模型 进程,线程
5, IO多路复用 select、poll、epoll

select 使用的步骤

相关函数

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,struct timeval *timeout);
功能:完成指定描述符集合中有效描述符的动态检测。
该函数具有阻塞等待功能,在函数执行完毕后
目标测试集合中将只保留最后有数据的描述符。

参数:nfds 描述符的上限值,一般是链接后描述符的最大值+1;
     readfds 只读描述符集
     writefds 只写描述符集
     exceptfds 异常描述符集
以上三个参数都是 fd_set * 的描述符集合类型
timeout 检测超时 如果是NULL表示一直检测不超时 。
返回值:超时 0
       失败 -1
       成功 >0

辅助函数
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。

int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。

void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。

void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。

注意事项:

1 . select 函数会修改readfds 集合。如果是循环调用select ,需要清除标志位。
2. nfds 进程中最大文件描述的那个整数。 可以直接写1024;
3. select如果是超时版本,在调用select前,都需要重新设置时间。
4 select 集合最大检测 1024个;

以下是TCP通过select实现的多客户端连接

server端:

#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include<sys/select.h>
typedef struct sockaddr *(SA);
int	main(int argc, char **argv)
{
    // 1 打开网络设备,获得文件描述(套接字)
    // 监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser,cli;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    // 2 给套接字绑定 ip +port ,方便客户端找到服务器
    int ret = bind(listfd, (SA)&ser,sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }

    // 3 进入监听状态 (套接字进入可以被链接的状态)
    // 参数2  ,进行三次握手的排队数
    listen(listfd,3);

    socklen_t len = sizeof(cli);

    // sel_1  create set
    fd_set rd_set,tmp_set;

    // sel2_ add fd
    FD_ZERO(&rd_set);
    FD_ZERO(&tmp_set);
    FD_SET(listfd, &tmp_set);
    int maxfd = listfd;
    while (1)
    {
        // sel5 清除sel标志位
        rd_set = tmp_set;
        // sel3 wait event 等待事件到来
        int sel_ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        int i = 0;
        for (i = 0; i < maxfd + 1; i++)
        {
            if (FD_ISSET(i, &rd_set) && i == listfd){//当客户端想建立连接时走这
                // 4 建立链接  (触发三次握手)
                // 通信套接字,表示从服务端来看,conn 是客户端的套接字
                int conn = accept(listfd, (SA)&cli,&len);
                if (conn == -1)//客户端想断开连接
                {
                    perror("accept");
                    close(conn);
                    continue;
                }
                FD_SET(conn, &tmp_set);
                //每次需要更新最大的fd号,传进循环条件
                maxfd = conn > maxfd ? conn : maxfd;
            }
            if (FD_ISSET(i, &rd_set) && i != listfd){
                int conn = i;
                char buf[1024] = {0};
                // 5 接收数据
                // 返回值  >0 实际收到的字节数  ==0  对方断开 -1 错误
                int rd_ret = recv(conn, buf, sizeof(buf), 0);
                if (rd_ret <= 0)
                {
                    printf("cli offline\n");
                    FD_CLR(conn, &tmp_set);
                    close(conn);
                    continue;;
                }
                printf("from client:%s\n",buf);
                time_t tm;
                time(&tm);
                sprintf(buf, "%s %s", buf,ctime(&tm));
                // 6 发送数据
                send(conn, buf, strlen(buf), 0);
            }
        }  
    }

    close(listfd);
    //close(conn);
    return 0;
}

client端:

#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("connect");
        return 1;
    }

    while(1){
        char buf[1024] = "hello, this is a tcp test";
        send(sockfd, buf, strlen(buf), 0);
        bzero(buf, sizeof(buf));
        int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
        if (rd_ret <= 0)
        {
            break;
        }
        printf("from server:%s\n",buf);
        sleep(1);

    }
    close(sockfd);
    return 0;
}

以下是UDP通过select实现聊天室的具体过程。

a端:

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

typedef struct sockaddr* (SA);

int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET,SOCK_DGRAM,0);
    if (udpfd == -1)
    {
        perror("socket");
        return 1;
    }
    // 2. 给套接字 绑定ip ,端口号
    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser, cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    //指定网络类型
    ser.sin_family = AF_INET; // ipv4 
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000); // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);

    char buf[1024] = {0};
    //得到客户端的 地址
    recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
    printf("%s\n",buf);

    fd_set rd_set,tmp_set;
    // sel2_ add fd
    FD_ZERO(&rd_set);
    FD_ZERO(&tmp_set);
    FD_SET(0, &tmp_set);
    FD_SET(udpfd, &tmp_set);
    int maxfd = udpfd;
    
    while (1)
    {
        rd_set = tmp_set;
        select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        int i = 0;
        for (i = 0; i < maxfd + 1; i++)
        {
            char buf[1024] = {0};
            if (FD_ISSET(i, &rd_set) && i == udpfd)//套接字走这
            {
                recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    printf("接收退出\n");
                    close(udpfd);
                    exit(0);
                }
                printf("from B:%s\n",buf);
                
            }
            if (FD_ISSET(i, &rd_set) && i == 0)
            {
                printf("to B:");
                fflush(stdout);
                fgets(buf, sizeof(buf), stdin);
                sendto(udpfd, buf, sizeof(buf), 0, (SA)&cli, len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    printf("发送退出\n");
                    close(udpfd);
                    exit(0);
                }
            }
        }

    }
    return 0;
}

b端:

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

typedef struct sockaddr *(SA);

int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    //指定网络类型
    ser.sin_family = AF_INET;     // ipv4
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");

    socklen_t len = sizeof(ser);

    char buf[1024] = "咱们开始聊天吧";
    //告知服务端自己的 地址
    sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
    
    fd_set rd_set,tmp_set;
    // sel2_ add fd
    FD_ZERO(&rd_set);
    FD_ZERO(&tmp_set);
    FD_SET(0, &tmp_set);
    FD_SET(udpfd, &tmp_set);
    int maxfd = udpfd;

    while (1)
    {
        rd_set = tmp_set;
        select(maxfd + 1, &rd_set, NULL, NULL, NULL);
        int i = 0;
        for (i = 0; i < maxfd + 1; i++)
        {
            char buf[1024] = {0};
            if (FD_ISSET(i, &rd_set) && i == udpfd)//套接字走这
            {
                recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&ser, &len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    printf("接收退出\n");
                    close(udpfd);
                    exit(0);
                }
                printf("from B:%s\n",buf);
                
            }
            if (FD_ISSET(i, &rd_set) && i == 0)
            {
                printf("to B:");
                fflush(stdout);
                fgets(buf, sizeof(buf), stdin);
                sendto(udpfd, buf, sizeof(buf), 0, (SA)&ser, len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    printf("发送退出\n");
                    close(udpfd);
                    exit(0);
                }
            }
        }

    }

    close(udpfd);
    return 0;
}

epoll
epoll 使用步骤

相关函数

int epoll_create(int size);

功能:创建一个集合(二叉树) ,存续需要被检测的文件描述符。

参数:size ,指定集合可以储存文件描述符的个数。

返回值:
成功 >0 表示对应二叉树的文件描述符( 用于表示某个资源)
失败 -1

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:对epoll 集合进行相关的操作,具体操作由op 指定。

参数:
epfd,需要被操作的集合
op :
EPOLL_CTL_ADD ,向集合中添加文件描述符
EPOLL_CTL_DEL ,向集合中删除文件描述符
fd ,需要 添加或删除的那个文件描述符
event , 结构体 ,
struct epoll_event {
uint32_t events; EPOLLIN(read)/EPOLLOUT(write)
特征:
服务器的并发模型
进程版本
epoll_data_t data; 用户自定义的数据,查找文件描述符的时候会使用到
};

返回值:
成功 ==0
失败 -1

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int
timeout);
功能: 等待io事件(可能是读,写,错误)的到来。

参数:
epfd,需要被检测的集合
events: 这个是输出的集合,当有io设备准备就绪(可读,可写)。系统会把这些设备对应的文件描述符,复
制到这个集合中。
maxevents, 在一次检测中,可以同时复制到events 中的 数量。
timeout 是超时值的设置。 -1 阻塞
5000 == 5s
0 非阻塞

返回值:
成功 >0
超时 ==0
失败 -1
1 . 存储文件描述符集合的个数,不再受1024 限制
2 .检测机制是 主动上报 ,不会随着文件描述的增多,而效率下降
3 。使用共享内存,避免集合在用户层和内核层 多次复制。
4. 当epoll_wait 返回后,准备就绪的文件描述符都储存在 events 集合中,相对容易查找。
1 .僵尸进程的回收(信号的方式)
2. 文件描述符的回收 listfd conn ,对于父子进程的差异

TCP同样可以通过epoll实现多端连接

server端:

#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <pthread.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <time.h>
#include<sys/epoll.h>
typedef struct sockaddr *(SA);

int add_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
        return ret;
    }
    return 0;
}

int del_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
    }
    return 0;
}

int	main(int argc, char **argv)
{
     // 1 打开网络设备,获得文件描述(套接字)
    // 监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listfd)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser, cli;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50001);
    ser.sin_addr.s_addr = INADDR_ANY;
    // 2 给套接字绑定 ip +port ,方便客户端找到服务器
    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {
        perror("bind");
        return 1;
    }

    // 3 进入监听状态 (套接字进入可以被链接的状态)
    // 参数2  ,进行三次握手的排队数
    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    struct epoll_event rev[10];
    int epfd = epoll_create(10);
    if (epfd == -1)
    {
        perror("epoll_create");
        return 1;
    }
    add_fd(epfd, listfd);

    while(1){

        int ep_ret = epoll_wait(epfd, rev, 10, -1);
        int i = 0;
        for (i = 0; i < ep_ret; i++)
        {
            if (rev[i].data.fd == listfd)// 三次握手
            {
                // 4 建立链接  (触发三次握手)
                // 通信套接字,表示从服务端来看,conn 是客户端的套接字
                int conn = accept(listfd, (SA)&cli, &len);
                if (conn == -1)
                {
                    perror("conn");
                    close(conn);
                    continue;
                }
                add_fd(epfd, conn);
            }
            else
            {
                char buf[1024] = {0};
                int conn = rev[i].data.fd;
                // 5 接收数据
                // 返回值  >0 实际收到的字节数  ==0  对方断开 -1 错误
                int rd_ret = recv(conn, buf, sizeof(buf), 0);
                if (rd_ret == -1)
                {
                    printf("cli offline");
                    del_fd(epfd, conn);
                    close(conn);
                    continue;
                }
                // 5.5  处理数据
                printf("cli:%s\n", buf);
                time_t tm;
                time(&tm);
                sprintf(buf, "%s %s", buf, ctime(&tm));
                // 6 发送数据
                send(conn, buf, strlen(buf), 0);

            }
        }

    }
    close(listfd);
    return 0;
}

client端:


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

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
    // 1 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50001);
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");
    // 2 ,主动连接服务器
    int ret = connect(sockfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {
        perror("connect");
        return 1;
    }
    
    while (1)
    {
        char buf[512]="hello,this is tcp test";
        // 3. 发送数据  返回值 >0 实际发送的字节数 ==0 网络状态不好,没有发出数据  -1错误
        send(sockfd,buf,strlen(buf),0);
        
        bzero(buf,sizeof(buf));
        // 4. 接收数据  >0 实际接收的字节数  ==0 表示断开 -1 错误
        int rd_ret = recv(sockfd,buf,sizeof(buf),0);
        if(rd_ret<=0)
        {
            break;
        }
        printf("from ser:%s\n",buf);
        sleep(1);
    }
    // 5.断开连接
    close(sockfd);

    return 0;
}

同样用UDP通过epoll实现聊天室:

a端:

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

typedef struct sockaddr* (SA);

int add_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
        return ret;
    }
    return 0;
}

int del_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
    }
    return 0;
}
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET,SOCK_DGRAM,0);
    if (udpfd == -1)
    {
        perror("socket");
        return 1;
    }
    // 2. 给套接字 绑定ip ,端口号
    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser, cli;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    //指定网络类型
    ser.sin_family = AF_INET; // ipv4 
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000); // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = INADDR_ANY;
    //套接字绑定地址结构体 IP地址和端口号
    int ret = bind(udpfd, (SA)&ser, sizeof(ser));
    if (ret == -1)
    {
        perror("bind");
        return 1;
    }
    socklen_t len = sizeof(cli);
  
    struct epoll_event rev[2];
    int epfd = epoll_create(2);
    if (epfd == -1)
    {
        perror("epoll_create");
        return 1;
    }
    add_fd(epfd, udpfd);
    add_fd(epfd, 0);

    char buf[1024] = {0};
    //得到客户端的 地址
    recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
    printf("%s\n",buf);
    while (1)
    {
        int ep_ret = epoll_wait(epfd, rev, 2, -1);
        int i = 0;
        for (i = 0; i < ep_ret; i++)
        {
            char buf[1024] = {0};
            if (rev[i].data.fd == udpfd)
            {
                recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&cli, &len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    del_fd(epfd, udpfd);
                    close(udpfd);
                    exit(0);
                }
                printf("from B:%s\n",buf);
            }
            else
            {
                printf("to B:");
                fflush(stdout);
                fgets(buf, sizeof(buf), stdin);
                sendto(udpfd, buf, sizeof(buf), 0, (SA)&cli, len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    del_fd(epfd, udpfd);
                    close(udpfd);
                    exit(0);
                }
            }
        }

    }
    return 0;
}

b端:

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

typedef struct sockaddr *(SA);

int add_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
        return ret;
    }
    return 0;
}

int del_fd(int epfd,int fd){
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev);
    if (ret == -1)
    {
        perror("add_fd");
    }
    return 0;
}
int	main(int argc, char **argv)
{
    // 1. internet , udp, 默认协议
    int udpfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == udpfd)
    {
        perror("socket");
        return 1;
    }

    // man 7 ip 查询 ipv4 的地址结构体
    struct sockaddr_in ser;  // 服务器的地址结构体, 客户端的地址结构体
    bzero(&ser, sizeof(ser));
    //指定网络类型
    ser.sin_family = AF_INET;     // ipv4
    //初始化端口号,因为网络设备的大端存储的,所以需要转换一下
    ser.sin_port = htons(50000);  // host to net short 小端转大端
    //初始化ip地址
    ser.sin_addr.s_addr = inet_addr("127.0.0.1");

    socklen_t len = sizeof(ser);

    struct epoll_event rev[2];
    int epfd = epoll_create(2);
    if (epfd == -1)
    {
        perror("epoll_create");
        return 1;
    }
    add_fd(epfd, udpfd);
    add_fd(epfd, 0);

    char buf[1024] = "咱们开始聊天吧";
    //告知服务端自己的 地址
    sendto(udpfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));

    while (1)
    {
        int ep_ret = epoll_wait(epfd, rev, 2, -1);
        int i = 0;
        for (i = 0; i < ep_ret; i++)
        {
            char buf[1024] = {0};
            if (rev[i].data.fd == udpfd)
            {
                recvfrom(udpfd, buf, sizeof(buf), 0, (SA)&ser, &len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    del_fd(epfd, udpfd);
                    close(udpfd);
                    exit(0);
                }
                printf("from A:%s\n",buf);
            }
            else
            {
                printf("to A:");
                fflush(stdout);
                fgets(buf, sizeof(buf), stdin);
                sendto(udpfd, buf, sizeof(buf), 0, (SA)&ser, len);
                if (strcmp(buf, "quit\n") == 0)
                {
                    del_fd(epfd, udpfd);
                    close(udpfd);
                    exit(0);
                }
            }
        }

    }
    close(udpfd);
    return 0;
}

服务器的并发模型

进程版本

/**
 * @file 01ser.c 进程版本的并发  tcp 服务器
 1 .僵尸进程的回收
 2. 文件描述符的回收  listfd conn  ,对于父子进程的差异

 * @author yashiro (rage_yas@hotmail.com)
 * @brief
 * @version 0.1
 * @date 2026-03-09
 *
 * @copyright Copyright (c) 2026
 *
 */
#include <endian.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <sys/wait.h>
#include <signal.h>

typedef struct sockaddr *(SA);

void handler(int num){
    wait(NULL);
}

int	main(int argc, char **argv)
{
    signal(SIGCHLD, handler);//为了回收时不阻塞,用信号来回收子进程
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listfd)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser, cli;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    // 2 给套接字绑定 ip +port ,方便客户端找到服务器
    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {
        perror("bind");
        return 1;
    }

    // 3 进入监听状态 (套接字进入可以被链接的状态)
    // 参数2  ,进行三次握手的排队数
    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    while (1)
    {
        int conn = accept(listfd, (SA)&cli, &len);
        if (conn == -1)
        {
            perror("conn");
            close(conn);//没链接成功继续连
            continue;
        }
        pid_t pid = fork();//一般情况下,死循环中的fork会导致锁死,但是这之前调用了accept,所以会先阻塞住。
        if (pid > 0)
        {
            close(conn);//为了节省资源,父进程只负责三次握手,只需要用到listfd,所以关闭通信套接字
        }
        else if (pid == 0)
        {
            close(listfd);//为了节省资源,子进程只负责通信,只需要用到conn,所以关闭监听套接字
            while (1) {
                char buf[1024] = {0};
                int rd_ret = recv(conn, buf, sizeof(buf), 0);
                if (rd_ret <= 0)
                {
                    perror("cli offline\n");
                    exit(0);
                }
                // 5.5  处理数据
                printf("cli:%s\n", buf);
                time_t tm;
                time(&tm);
                sprintf(buf, "%s %s", buf, ctime(&tm));
                // 6 发送数据
                send(conn, buf, strlen(buf), 0);
            }     
        }
        else
        {
            perror("fork");
            continue;
        }
    }
    close(listfd);
    return 0;
}

线程版本

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

typedef struct sockaddr*(SA);
sem_t sem_arg;

void* th(void* arg){
    int conn = *(int*)arg;
    sem_post(&sem_arg);//在线程拿到conn后释放信号量,保证同步操作
    // 分离属性, 当线程结束后,栈区自动由系统回收
    pthread_detach(pthread_self());//设置分离属性
    while (1)
    {
        char buf[512] = {0};
        // 5 接收数据
        // 返回值  >0 实际收到的字节数  ==0  对方断开 -1 错误
        int rd_ret = recv(conn, buf, sizeof(buf), 0);
        if (rd_ret <= 0)
        {
            printf("cli offline\n");
            close(conn);
            break;
        }
        // 5.5  处理数据
        printf("cli:%s\n", buf);
        time_t tm;
        time(&tm);
        sprintf(buf, "%s %s", buf, ctime(&tm));
        // 6 发送数据
        send(conn, buf, strlen(buf), 0);
    }
}
int	main(int argc, char **argv)
{
     // 1 打开网络设备,获得文件描述(套接字)
    // 监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listfd)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in ser, cli;
    //清空 结构体
    bzero(&ser, sizeof(ser));
    bzero(&cli, sizeof(cli));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    // 2 给套接字绑定 ip +port ,方便客户端找到服务器
    int ret = bind(listfd, (SA)&ser, sizeof(ser));
    if (-1 == ret)
    {
        perror("bind");
        return 1;
    }

    // 3 进入监听状态 (套接字进入可以被链接的状态)
    // 参数2  ,进行三次握手的排队数
    listen(listfd, 3);

    socklen_t len = sizeof(cli);
    sem_init(&sem_arg, 0, 0);//设初值为0
    while (1)
    {
        int conn = accept(listfd, (SA)&cli, &len);
        if (conn == -1)
        {
            perror("conn");
            close(conn);
            continue;
        }

        pthread_t tid;
        pthread_create(&tid, NULL, th, &conn);
        //这里不用pthread_join是因为,会阻塞在这,导致其他客户端无法链接进来,所以需要设置分离属性

        //由于线程接受conn可能会被和上面accept的conn影响
        //(conn可能用同一个栈空间,conn还没完成传给线程,accept又修改了conn的值)所以传入conn和accept应该是同步操作,需要信号量
        // 确保 conn 参数一定在th现存中,被保存下来
        sem_wait(&sem_arg);//因为信号量初值为0,线程中接收到conn后post,信号量变为1,这里的sem_wait是给accept服务的

    }
    sem_destroy(&sem_arg);
    // 7 断开链接
    close(listfd);
    return 0;
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐