linux网络编程自定义协议和多进程多线程并发

1.三次握手及后面过程

计算机A是客户端, B是服务端

1.1三次握手:

1客户端给服务端SYN报文

2服务端返回SYN+ACK报文

3客户端返回ACK报文

客户端发完ACK后加入到服务端的维护队列中,accept()调用后就能和客户端建立连接,然后建立通讯

1.2关闭连接过程

客户端发送FIN报文给服务端,就是用来关闭连接的请求

服务器端返回ACK报文

服务端发送FIN报文

服务端返回ACK报文

2.自定义协议

2.1TCP模型 ------作为自定义协议参考

2.2,自定义协议的Msg结构体加上了头部,校验码和体部

1.Msg.h如下

cpp 复制代码
#ifndef __MSG_H__
#define __MSG_H__

#include <sys/types.h>

typedef struct{
    //协议头部
    char head[10]; //头部
    char checknum;  //校验码,验证发送方和接收方是否一样
    //协议体部
    char buff[512];  //数据
}Msg;

/*
 *发送一个基于自定义协议的message
 *发送的数据存放在buff中
 */
extern int write_msg(int sockfd, char *buff, 
    size_t len);

/*
 *读取一个基于自定义协议的message
 *读取的数据存放在buff中
 */
extern int read_msg(int sockfd, char *buff,
     size_t len);

#endif

2.Msg.c如下

cpp 复制代码
#include "msg.h"
#include <unistd.h>
#include <string.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

//计算校验码,加的是每个字符的ASCII码值
//unsigned char s是0~255范围,溢出了会回绕
//通过这样检验消息头部和体部加每个字符ASCII码,溢出回绕
//如果传输无差错最终得到一个定值
static unsigned char msg_check(Msg *message)
{
    unsigned char s = 0;
    int i;
    //遍历head,和buff数组
    for(i = 0; i < sizeof(message->head); i++){
        s += message->head[i];
    }
    for(i = 0; i < sizeof(message->buff); i++){
        s += message->buff[i];
    }
    return s;
} 

/*
 *发送一个基于自定义协议的message
 *发送的数据存放在buff中
 */
int write_msg(int sockfd, char *buff, 
    size_t len)
{
    Msg message;//定义发送的信息
    memset(&message, 0, sizeof(message));
    //头部
    strcpy(message.head, "jwp202411");
    //体部,用于内存拷贝,len指定字节
    memcpy(message.buff, buff, len);
    //校验码
    message.checknum = msg_check(&message);
    //通过文件IO write写入管道
    if(write(sockfd, &message,
     sizeof(message)) != sizeof(message)){
        return -1;  //写入出错
     }

    return 0;//写入成功
}

/*
 *是结构体message的
 *读取一个基于自定义协议的message
 *读取的体部数据存放在buff中
 */
int read_msg(int sockfd, char *buff,
     size_t len)
{
    Msg message;
    memset(&message, 0, sizeof(message));
    size_t size;
    //读socket套接字的描述符,发送方也是和
    //接收方发送的统一message结构体格式接收方,这里是这样的
    if(size = read(sockfd, &message, 
        sizeof(message) < 0)){
            perror("message read error");
            return -1;
        }else if(size == 0){
             //写的一端关闭读的一端会读到0个
            return 0;
        }

    //进行校验码验证
    //验证接收到的发送方校验和读取message计算的
    //是否相同
    //为了验证发送是否出现少发或者发错
    unsigned char s = msg_check(&message);
    if((s == (unsigned char)message.checknum) 
        && (!strcmp("jwp202411", message.head))){
    //验证接收到的数据没问题就拷贝接收到的字节到buff中
        memcpy(buff, message.buff, len);
        //返回接收到的字节数
        return sizeof(message);
    }  
    return -1;
}

2.3memcpy()用于拷贝内存,strcpy()用于拷贝字节

cpp 复制代码
memcpy() 和 strcpy() 是 C 语言中用于内存操作的两个标准库函数,它们各自有不同的用途和特点。

memcpy()
memcpy() 函数用于复制内存区域。它从源内存区域复制指定数量的字节到目标内存区域。函数原型如下:

c
深色版本
void *memcpy(void *dest, const void *src, size_t n);
dest:指向目标对象的指针,即数据将要被复制到的地方。
src:指向源对象的指针,即数据将要从这里被复制。
n:要复制的字节数。
这个函数不会检查源字符串是否以空字符 \0 结束,也不会添加终止符。因此,它适用于复制任何类型的内存块,包括非字符数组。

strcpy()
strcpy() 函数专门用于复制字符串。它从源字符串复制字符(包括终止空字符 \0)到目标字符串。函数原型如下:

c
深色版本
char *strcpy(char *dest, const char *src);
dest:指向目标字符串的指针,该字符串应当有足够的空间来容纳源字符串的所有字符加上终止符。
src:指向源字符串的指针,该字符串必须是以空字符 \0 结尾的。
strcpy() 会自动在复制的目标字符串末尾添加一个空字符 \0,确保目标字符串也是以空字符结尾的。

使用注意事项
缓冲区溢出:使用 strcpy() 时需要特别注意目标缓冲区是否有足够的空间来存放源字符串以及终止符,否则可能会导致缓冲区溢出,这是一个常见的安全漏洞。
类型安全:memcpy() 可以用于复制任何类型的数据,而不仅仅是字符数组。但是,当复制结构体等复杂数据类型时,需要确保目标和源的数据类型兼容,并且复制的字节数正确。
性能差异:对于简单的字符串复制任务,strcpy() 可能更方便,因为它自动处理了字符串终止符。但对于已知大小的内存块复制,memcpy() 通常更加高效。
总之,选择使用哪个函数取决于具体的应用场景和需求。如果只是简单地复制字符串,strcpy() 是一个合适的选择;而对于更复杂的内存操作或需要精确控制复制字节数的情况,则应该使用 memcpy()。

2.4编译命令

cpp 复制代码
gcc -o obj/msg.o -Iinclude -c src/msg.c

提供的命令 gcc -o obj/msg.o -Iinclude -c src/msg.c 是用于编译 C 源文件 src/msg.c 并生成目标文件 obj/msg.o

cpp 复制代码
1,gcc:
这是 GNU 编译器集合(GCC)的命令行工具,用于编译 C、C++ 等语言的源代码。
-o obj/msg.o:
-o 选项用于指定输出文件的名称。
2,obj/msg.o 是输出文件的路径和名称。在这个例子中,编译器会生成一个名为 msg.o 的目标文件,并将其放在 obj 目录中。
3,-Iinclude:
-I 选项用于指定额外的头文件搜索路径。
include 是目录名,表示编译器会在 include 目录中查找头文件。这对于包含自定义头文件非常有用。
-c:
-c 选项告诉编译器只进行编译和汇编步骤,生成目标文件(.o 文件),但不进行链接。
这意味着编译器不会生成最终的可执行文件,而是生成一个中间目标文件。
4,src/msg.c:
这是要编译的源文件的路径和名称。
src/msg.c 表示源文件位于 src 目录中,文件名为 msg.c。

编译结果

3.文件IO,管道IO,网络编程的IO

1.当服务端调用read(),但没有接收到数据,会阻塞等待信息来临,除非终止等待的客户端进程

当服务端调用read(sockfd ....);但客户端没有发送消息的方式过来服务器端会阻塞,这样再有其他客户端请求服务端连接只被Listen()到了监听队列中,但服务器端被前一个连接上了的客户端阻塞,服务端就无法调用accept()从队列中与客户端连接

所以要并发处理客户端请求,通过服务端一个进程,由此引出服务端并发性处理

1.多进程模型

服务端进程看作父进程,第一个客户端过来创建子进程负责和客户端通讯,第二个客户端过来创建一个子进程也是负责和这个客户端通讯,父进程就光accept()负责和客户端进行连接

2.客户端与服务端的连接类似管道,

3.do_service()服务器端成功读取把读取到的信息写回去通过wirte_msg()

不完整管道:当读端关闭,往写端写入数据,会产生SIGPIPE信号,同时错误编号设置errno = EPIPE, 客户端正好关闭了,服务端正好写入的时候,

2.多线程模型

3.I/O多路转换

4.多进程并发处理及代码

1.echo_tcp_server.c如下

cpp 复制代码
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include "msg.h"
#include <errno.h>

int sockfd;  //服务器端套接字描述符

//处理ctrl+c终止服务器函数
void sig_handler(int signo)
{
    if(signo == SIGINT){
        printf("server close\n");
        /*步骤6,关闭sockt*/
        close(sockfd);
        exit(0);
    }
    if(signo == SIGCHLD){
        //子进程结束父进程回收它
        printf("SIGCHLD child processing dead..\n");
         //等待任意一个子进程终止,并回收子进程占用资源
        wait(0);
    }
}

//输出连接上来的客户端的相关信息
void out_addr(struct sockaddr_in *client)
{
    //获得端口,是个网络字节序(转成主机字节序)
    int port = ntohs(client->sin_port);
    char ip[16];//3*4+3个点+一个结束字符=16
    memset(ip, 0, sizeof(ip));
    //将IP地址从网络字节序转换成点分十进制
    inet_ntop(AF_INET, &client->sin_addr.s_addr, ip, sizeof(ip));
    printf("client: %s(%d) connected\n", ip, port);
}

void do_service(int fd)
{
    /*和客户端进行读写操作(双向通信)*/
    char buff[512];
    while(1){
        memset(buff, 0, sizeof(buff));//清零
        printf("start read and write...\n");
        ssize_t size;
        //根据协议读取到buff
        if((size = read_msg(fd, buff, sizeof(buff))) < 0){
            //协议错误
            perror("protocol error");
            break;
        } else if(size == 0) {
            printf("pipe no connect\n");
            //返回0则表示read到的为0,写端没有连接或者发送,退出do_service
            break;//结束do_service
        } else {
            //服务器端把接收到的数据输出
            printf("%s\n", buff);
            //发送读到的msg给客户端
            if(write_msg(fd, buff, size) < 0){
                //不完整管道会设置errno = EPIPE,也会产生SIGPIPE信号
                //也可以捕获SIGPIPE信号
                if(errno == EPIPE){
                    printf("pipe incomplete");
                    //对方已经关闭了读管道,则管道未连接
                    break;
                }
                perror("write_msg protocol error");
            }
            printf("write success back to client\n");
        }
    }
}

int main(int argc, char *argv[])
{
    //服务器端指定端口,要监听,命令行中传递进去
    if(argc < 2){
        printf("usage: %s #port\n", argv[0]);
        exit(1);
    }
    //登记一个信号SIGINT,ctrl+c终止服务器端
    if(signal(SIGINT, sig_handler) == SIG_ERR)
    {
        perror("signal sigint error");
        exit(1);
    }
    //子进程终止的信号
    if(signal(SIGCHLD, sig_handler) == SIG_ERR)
    {
        perror("signal sigchld error");
        exit(1);
    }

    /*步骤1,创建socket(套接字),
    socket创建在内核中,是一个结构体,AF_INET:IPV4
    SOCK_STREAM:tcp协议  最后参数0
    */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    /*步骤2:调用bind将socket
    (包括ip,port)进行绑定*/
    //sockaddr_in,因特网的专用地址,sockaddr是通用地址
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));//清空
    //往地址中填入ip,port,internet地址族类型
    serveraddr.sin_family = AF_INET;
    //端口号需要为16位网络字节序,atoi()将字符串转换为int
    serveraddr.sin_port = htons(atoi(argv[1]));
    //网络字节序IP地址,INADDR_ANY响应所有网卡的请求
    //一台主机有多个网卡,多个IP地址,响应所有网卡来源上的客户端请求*/
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    //绑定,第二个参数需要强转为struct sockaddr*
    if(bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0){
        perror("bind error");
        exit(1);
    }
    /*步骤3,调用listen函数启动监听(指定port端口监听)通知系统去接受来自客户端的连接请求
    ,将接收到的客户端连接放在队列中backlog,指定客户端排队队列长度*/
    if(listen(sockfd, 10) < 0)
    {
        perror("listen error");
        exit(1);
    }
    /*步骤4,获得某一个客户端的连接
    /调用accept函数从队列中获得一个 客户端的请求连接,并返回新的socket描述符, 在内部会新创建socket返回描述符
    ,通过此描述符和客户端描述符进行通信,若没有客户端连接,调用accept()会阻塞,直到获得一个客户端连接并返回新的socket描述符
    第二个参数用于接收客户端地址信息*/
    struct sockaddr_in clientaddr;
    socklen_t clientaddr_len = sizeof(clientaddr);
    while(1){
        int fd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len);

        if(fd < 0){
            perror("accept error");
            continue;//结束本次循环,继续下一次
        }
        /*步骤5,启动子进程调用IO函数(read/write)和连接的客户端进行双向的通讯*/
        //父进程由accept()产生的fd会复制一份给子进程
        pid_t pid = fork(); 
        if(pid < 0){
            //创建出错
            continue;
        } else if(pid == 0){ //子进程终止会产生SIG_CHILD信号
        //子进程不会继承父进程的函数调用栈。也就是说,
        //子进程不会继承父进程当前正在执行的函数调用状态
            //输出接收到的客户端地址
            out_addr(&clientaddr);
            //do_service()和客户端进行通讯服务,用于和客户端连接
            do_service(fd); //子进程继承fd
            //步骤6,关闭针对客户端的socket,
            close(fd); 
            printf("Child process exiting after handling client request.\n");
            exit(0);  // 子进程处理完后退出
        } else { //父进程
            close(fd); //fd被子进程创建两次
        }
    }

    return 0;
}

值得注意的是,服务端fork()子线程处理服务端与客户端通讯,子进程要复制父进程的代码段,包括while()循环,所以子进程break的是子进程的while()循环,

子进程获得父进程数据空间,堆和栈的副本,子进程继承父进程所有的文件描述符,但不继承accept()函数和fork()函数,继承了父进程的while(1)循环,所以父进程处理accept()和客户端队列的连接,子进程处理do_service(), 子进程不会继承父进程的函数调用栈。也就是说,子进程不会继承父进程当前正在执行的函数调用状态,

2.echo_tcp_client.c如下

cpp 复制代码
#include <netdb.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include "msg.h"

//自定义协议客户端

int sockfd;

//处理ctrl+c终止客户端函数
void sig_handler(int signo)
{
    if(signo == SIGINT){
        printf("server close\n");
        /*步骤6,关闭sockt*/
        close(sockfd);
        exit(0);
    }
}

int main(int argc, char *argv[])
{
    if(argc < 3){
        printf("usage: %s ip port\n", argv[0]);
        exit(1);
    }

    if(signal(SIGINT, sig_handler) == SIG_ERR){
        perror("signal sigint error");
        exit(1);
    }

    /*步骤1:创建socket*/
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){
        perror("socket error");
        exit(1);
    }
    

    //因特网专用结构体
    //往sockaddr中填入ip.port和地址族类型ipv4
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET; //IPV4
    serveraddr.sin_port = htons(atoi(argv[2]));
    //将点分十进制的ip地址转换为网络字节序
    //第二个参数是输入的字符串形式ip地址,转换到第三个参数(网络字节序)
    //第三个参数是serceraddr.sin_addr.s_addr的地址
    int res = inet_pton(AF_INET, argv[1],
     &serveraddr.sin_addr.s_addr);
    if(res < 0){
        perror("inet_pton error");
        exit(1);
    }
    /*步骤2:客户端调用connect函数连接到服务器端*/
    if(connect(sockfd, (struct sockaddr*)&serveraddr, 
        sizeof(serveraddr)) < 0){
        printf("connect error");
        exit(1);
    }

    /*步骤3:调用IO函数(read/write)和服务器端进行双向通信*/
    char buff[512];
    size_t size;
    char *prompt = ">";  //提示
    while(1){
        memset(buff, 0, sizeof(buff));
        //写提示到标准输出
        write(STDOUT_FILENO, prompt, 1);
        //获取标准输入要发送的信息
        size = read(STDIN_FILENO, buff, sizeof(buff) - 1);  //减1防止溢出
        if(size < 0) continue;  //结束本次循环,继续下一次
        //read函数读取存到buff里的字符不会加'\0'
        buff[size - 1] = '\0';

        //通过客户端创建的套接字描述符
        //自定义的write函数发送给服务端
        //发送实际读取的字节数
        if(write_msg(sockfd, buff, size - 1) == -1){
            //发送错误
            perror("write msg error");
            continue;
        }else{//发送成功,读取服务端返回的信息
            //printf("read from server\n");
            if(read_msg(sockfd, buff, 
                sizeof(buff)) < 0)
                {
                    printf("read error\n");
                    perror("read msg error");
                    continue;
                }else{
                    //读取服务器端返回信息成功
                    //正常读取,把客户端读到的数据输出到标准输出
                   //printf("read success\n");
                    printf("%s\n", buff);  // \n让标准输出从缓存中刷新到终端
                }
        }
    }

    /*步骤4:关闭socket*/
    close(sockfd);

    return 0;
}

3.编译和运行命令如下

1.编译命令

cpp 复制代码
gcc -o bin/echo_tcp_server -Iinclude obj/msg.o src/echo_tcp_server.c



 gcc -o bin/echo_tcp_client -Iinclude obj/msg.o src/echo_tcp_client.c 

2.运行命令

cpp 复制代码
 ./bin/echo_tcp_server 8888


./bin/echo_tcp_client 127.0.0.1 8888

3.客户端运行结果

4.服务端运行结果

5.多线程并发处理及代码

1.多线程比较多进程的好处:

多进程并发处理多客户端请求是有弊端的,多个客户端同时访问,服务端需要启动许多个子进程,每个子进程都要占用系统资源,导致服务端处理大并发速度变慢,最终导致服务端挂掉。

2.多线程服务器模型处理客户端并发处理

线程不太占用资源,服务器端可以针对连接的多个客户端,服务器端启动多个针对性的子线程,让各个子线程去针对性对应于某个客户端

所以可以基于多进程改进,accept()后将启动子进程改为启动子线程

有一个主控线程,通过主控线程去创建子线程,每个子线程都服务于某个客户端,所有子线程共享同一进程资源,子线程占有资源后要自动释放资源,比如栈空间的资源。

两种方式自动释放:

1:主控线程创建并启动子线程,主控线程调用pthread_join(),主控线程自己阻塞

子线程结束,它所占用资源被释放,当多个子线程都调用pthread_join(),会导致主控线程持续阻塞,导致无法处理新的客户端请求。accept(),主控线程不断循环做accept()负责服务端和客户端连接,所以调用pthread_join()释放占用资源是不合适的

2:以分离状态启动的子线程,当子线程运行结束,它占用的资源会自动释放,主控线程做循环调用accept();

综述:使用分离状态启动子线程,在主控线程循环调用accept()来负责服务器和客户端的连接,没有客户端连接到客户端时,while(1)循环先执行accept(),由于accept()在listen()监听队列没有找到已完成的连接请求(已完成的连接请求来自listen()的监听队列),就会阻塞while(1),一旦有另外的客户端请求连接,阻塞解除,以分离状态启动子线程,并传入accept()返回的socket描述符给线程运行函数,负责处理和客户端的通讯和显示客户端信息。

bind()负责绑定socket()创建的套接字到某个具体的IP地址(可以在初始网络因特网专用结构体把IP=INADDR_ANY就能响应各个IP的连接请求)和端口是自己从终端输入或者绑定的

3.处理并发的场景,不止分离状态启动子线程,在高级编程中,以IO多路转换

4还有非阻塞方式 fcntl

3.以分离状态启动子线程

pthread_tcp_server.c代码

多线程模型以分离状态处理客户端并发请求的场景

getpeername() ,以socket文件描述符,获得accept()返回的socket()描述符fd,来获取客户端的地址信息,端口号(网络字节序),IP地址(网络字节序)要通过inet_ntop()网络字节序转换为点分十进制字节序,然后打印出来

//从fd中获得连接的客户端相关信息
//存放在传入的addr第二个专用地址结构体中
if(getpeername(fd, (struct sockaddr*)&addr, &len) < 0){
perror("getpeername error");
return;
}
char ip[16];
memset(ip, 0, sizeof(ip));
int port = ntohs(addr.sin_port); //主机端口
//将网络字节序ip地址转换为点分十进制字符ip地址
inet_ntop(AF_INET,
&addr.sin_addr.s_addr, ip, sizeof(ip));
printf("%16s(%5d) closed!\n", ip, port);

cpp 复制代码
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include "msg.h"
#include <errno.h>
#include <pthread.h>

int sockfd;  //服务器端套接字描述符

//处理ctrl+c终止服务器函数
void sig_handler(int signo)
{
    if(signo == SIGINT){
        printf("server close\n");
        /*步骤6,关闭sockt*/
        close(sockfd);
        exit(0);
    }
}

//负责与客户端进行通讯
void do_service(int fd)
{
    /*和客户端进行读写操作(双向通信)*/
    char buff[512];
    while(1){
        memset(buff, 0, sizeof(buff));//清零
        ssize_t size;
        //根据协议读取到buff
        if((size = read_msg(fd, buff, sizeof(buff))) < 0){
            //协议错误
            perror("protocol error");
            break;
        } else if(size == 0) {
            printf("client breaked connection\n");
            //返回0则表示read到的为0,写端没有连接或者发送,退出do_service
            break;//结束do_service
        } else {
            //服务器端把接收到的数据输出
            printf("%s\n", buff);
            //发送读到的msg给客户端
            if(write_msg(fd, buff, size) < 0){
                //不完整管道会设置errno = EPIPE,也会产生SIGPIPE信号
                //也可以捕获SIGPIPE信号
                if(errno == EPIPE){
                    printf("pipe incomplete");
                    //对方已经关闭了读管道,则管道未连接
                    break;
                }
                perror("write_msg protocol error");
            }
        }
    }
}

//通过传入accept()返回的socket
//描述符获取客户端地址信息
void out_fd(int fd)
{
    //通过accept()返回的fd套接字描述符
    //获取客户端信息
    //专用sockaddr_in地址结构体
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    //从fd中获得连接的客户端相关信息
    //存放在传入的addr第二个专用地址结构体中
    if(getpeername(fd, (struct sockaddr*)&addr,
                 &len) < 0){
        perror("getpeername error");
        return;
    }
    char ip[16];
    memset(ip, 0, sizeof(ip));
    int port = ntohs(addr.sin_port); //主机端口
    //将网络字节序ip地址转换为点分十进制字符ip地址
    inet_ntop(AF_INET, 
    &addr.sin_addr.s_addr, ip, sizeof(ip));
    printf("%16s(%5d) closed!\n", ip, port);

}

//线程运行函数
void* th_fn(void *arg)
{
    int fd = (int)arg;
    //开始读写
    printf("start read and write...\n");
    do_service(fd);
    //do_service结束要么是客户端挂掉了,要么数据通信完毕了
    //输出相关客户端信息
    out_fd(fd);
    close(fd);

    return (void*)0;
}

int main(int argc, char *argv[])
{
    //服务器端指定端口,要监听,命令行中传递进去
    if(argc < 2){
        printf("usage: %s #port\n", argv[0]);
        exit(1);
    }
    //登记一个信号SIGINT,ctrl+c终止服务器端
    if(signal(SIGINT, sig_handler) == SIG_ERR)
    {
        perror("signal sigint error");
        exit(1);
    }


    /*步骤1,创建socket(套接字),
    socket创建在内核中,是一个结构体,AF_INET:IPV4
    SOCK_STREAM:tcp协议  最后参数0
    */
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    /*步骤2:调用bind将socket
    (包括ip,port)进行绑定*/
    //sockaddr_in,因特网的专用地址,sockaddr是通用地址
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));//清空
    //往地址中填入ip,port,internet地址族类型
    serveraddr.sin_family = AF_INET;
    //端口号需要为16位网络字节序,atoi()将字符串转换为int
    serveraddr.sin_port = htons(atoi(argv[1]));
    //网络字节序IP地址,INADDR_ANY响应所有网卡的请求
    //一台主机有多个网卡,多个IP地址,响应所有网卡来源上的客户端请求*/
    serveraddr.sin_addr.s_addr = INADDR_ANY;
    //绑定,第二个参数需要强转为struct sockaddr*
    if(bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0){
        perror("bind error");
        exit(1);
    }
    /*步骤3,调用listen函数启动监听(指定port端口监听)通知系统去接受来自客户端的连接请求
    ,将接收到的客户端连接放在队列中backlog,指定客户端排队队列长度*/
    if(listen(sockfd, 10) < 0)
    {
        perror("listen error");
        exit(1);
    }
    /*步骤4,获得某一个客户端的连接
    /调用accept函数从队列中获得一个 客户端的请求连接,并返回新的socket描述符, 在内部会新创建socket返回描述符
    ,通过此描述符和客户端描述符进行通信,若没有客户端连接,调用accept()会阻塞,直到获得一个客户端连接并返回新的socket描述符
    第二个参数用于接收客户端地址信息*/
    struct sockaddr_in clientaddr;
    socklen_t clientaddr_len = sizeof(clientaddr);

    //设置线程的分离属性
    pthread_attr_t attr;
    pthread_attr_init(&attr); //初始化
    //设置分离属性
    pthread_attr_setdetachstate(&attr, 
        PTHREAD_CREATE_DETACHED);

    while(1){
        //主控线程负责调用accept去获得客户端连接
        //会自动往第二个参数里面填入对应的客户端信息,IP,端口
        //也可从accept()返回的地址描述符获取客户端信息
        int fd = accept(sockfd, NULL, NULL);

        if(fd < 0){
            perror("accept error");
            continue;//结束本次循环,继续下一次
        }

        /*步骤5,启动子线程调用IO函数(read/write)
        和连接的客户端进行双向的通讯*/

        pthread_t th;
        int err;
        //以分离状态启动子线程
        //第三个是线程运行函数,第四个是传入运行函数的
        if((err = pthread_create(&th, &attr, th_fn, 
                (void*)fd)) != 0){
            perror("pthread create error");
        }
        //主控线程摧毁线程属性
        pthread_attr_destroy(&attr);
       
    }

    return 0;
}

以分离状态启动多线程处理并发运行结果

相关推荐
网络安全-杰克25 分钟前
网络安全概论
网络·web安全·php
怀澈12229 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
耗同学一米八1 小时前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题二
运维·网络·mariadb
skywalk81631 小时前
树莓派2 安装raspberry os 并修改成固定ip
linux·服务器·网络·debian·树莓派·raspberry
爱分享的码瑞哥1 小时前
Python爬虫中的IP封禁问题及其解决方案
爬虫·python·tcp/ip
_不会dp不改名_1 小时前
HCIA笔记3--TCP-UDP-交换机工作原理
笔记·tcp/ip·udp
co0t2 小时前
计算机网络(14)ip地址超详解
服务器·tcp/ip·计算机网络
C++忠实粉丝2 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
黑客Ela2 小时前
网络安全中常用浏览器插件、拓展
网络·安全·web安全·网络安全·php
qdprobot2 小时前
ESP32桌面天气摆件加文心一言AI大模型对话Mixly图形化编程STEAM创客教育
网络·人工智能·百度·文心一言·arduino