当前位置: 首页 > article >正文

TCP网络编程(二)—— 服务器端的编写

上篇文章我们学习了TCP的两种编程模式,这篇文章我们将开始编写服务器端的代码。完整代码在文章的最后。


首先,我们需要什么变量?

我们需要服务器端的套接字(socket),地址和端口(addr),还需要客户端的套接字(socket),地址和端口(addr)。
 

    #define Port 8888
    #define BACKLOG 2    
    int ss,sc;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;

    int err;
    pid_t pid;

这里:
ss表示服务器端套接字
sc表示客户端套接字
server_addr表示服务器端地址端口
client_addr表示客户端地址和端口
err是标志错误位,作为一些函数的返回值。
pid是进程的表示进程的ID,后面会解释,使用。
Port 是端口号,BACKLOG是监听队列的长度。


然后我们开始创建一个套接字:

    ss = socket(AF_INET,SOCK_STREAM,0);
    if(ss < 0){

        printf("socket error \n");
        return -1;
    }

AF_INET 指定使用 IPv4 地址
SOCK_STREAM 指定使用面向连接的 TCP 协议(如果是 UDP,则使用 SOCK_DGRAM)。
0 通常表示默认协议,与前两个参数相关(例如 SOCK_STREAM 默认对应 TCP 协议)。

其次是bind()绑定的准备工作(初始化服务器端的地址和端口):

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(Port);

bzero()将 server_addr 结构体初始化为 0,确保所有字段清零。避免未初始化的结构体包含随机数据,可以用 memset() 替代:memset(&server_addr, 0, sizeof(server_addr));

sin_family 是地址族字段,用于指定套接字类型。AF_INET 表示使用 IPv4 地址

sin_addr.s_addr 设置服务器的 IP 地址,inet_addr("127.0.0.1") 将字符串形式的 IP 地址转换为 32 位整数(网络字节序)。IP 地址 "127.0.0.1" 是回环地址,表示当前主机的本地通信。

sin_port:Port 表示端口号。htons() 函数将主机字节序(小端字节序)转换为网络字节序(大端字节序),确保跨平台通信时端口号的正确性。


然后是bind(),将服务器端的套接字与地址和端口绑定到一起,

    err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
    if(err < 0){
        printf("bind error \n");
        return -1;
    }

这里用强制类型转换,把struct sockaddr_in* 转化为struct sockaddr*,忘记了请看上篇文章。err 如果返回-1说明失败,打印错误信息。


然后是设置监听队列的长度:

    err = listen(ss,BACKLOG);
    if(err < 0){
        printf("listen error \n");
        return -1;
    }

 err 如果返回-1说明失败,打印错误信息。


最后最重要的主循环(accept等待请求,read读取,write写入):

 for(;;){
        socklen_t addrlen = sizeof(struct sockaddr);
        sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);

        if(sc < 0){
            continue;
        }

        pid = fork(); //开启子进程

        if(pid == 0){
            close(ss);

            char buffer [1024] = {0};
            char message [100] = {0};
            ssize_t size;

            for(;;){
                memset(buffer, 0, sizeof(buffer));
                size = read(sc,message,sizeof(message));

                if(size == 0){
                    return 0;
                }
                sprintf(buffer,"server have received %s \n",message);
                //write(sc,buffer,sizeof(buffer));
                write(1,buffer,strlen(buffer));

            }
            close(sc);

        }else if(pid > 0){
            close(sc);
        }else{
            printf("fork error \n");
        }
    }

咱们先看fork开启子进程之前的代码:

        socklen_t addrlen = sizeof(struct sockaddr);
        sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);

        if(sc < 0){
            continue;
        }

        pid = fork(); //开启子进程

新建addrlen变量,把套接字描述符,客户端地址和端口,客户端结构体大小传入,得到新的套接字描述符 sc  如果 sc 小于0,直接跳出整个大的循环,这说明没有产生新的套接字。 

那这个fork是什么意思呢?

fork() 是一个用于创建新进程的函数,特点是调用一次,返回两次。调用 fork() 后,当前进程会被复制,生成一个新进程,称为 子进程。相当于进程现在有两个:父进程和子进程都会收到fork()的返回值,在父进程中返回子进程的进程ID号,在子进程中返回0,失败返回-1。


现在咱们接着来看后面的代码:

 for(;;){

        //......
        //accept()阻塞等待连接
        //......

        pid = fork(); //开启子进程

        if(pid == 0){
            close(ss);

            char buffer [1024] = {0};
            char message [100] = {0};
            ssize_t size;

            for(;;){
                memset(buffer, 0, sizeof(buffer));
                size = read(sc,message,sizeof(message));

                if(size == 0){
                    return 0;
                }
                sprintf(buffer,"server have received %s \n",message);
                //write(sc,buffer,sizeof(buffer)); 写入sc客户端套接字
                write(1,buffer,strlen(buffer));

            }
            close(sc);

        }else if(pid > 0){
            close(sc);
        }else{
            printf("fork error \n");
        }
    }

首先我们要记得我们在刚刚创建了两个套接字,一个是监听之后的服务器端套接字ss,还有一个是accept()返回的客户端套接字sc。
可以看到我们在子进程中关闭了ss套接字,所以保留了客户端套接字,这说明子进程是用来和客户端保持通信连接的,在子进程中实现数据的处理,read 和write,循环处理。(read和write的用法不细说了,就是从套接字中读取或者写入,这里代码中的意思是从sc套接字中读取,写到标准输出1中,也就是打印到终端上
在父进程中我们关闭了sc套接字,所以保留了服务器端的套接字,这说明父进程是用来监听是否有新的客户端的,等待accept()接受有效值进而创建新的子进程的,因为不要忘了外面的一层大的循环。

看到这里,你可能会想:可不可以在父进程中创建连接,在子进程中进行监听呢?

理论上是可以的,但可能会出现问题:

首先fork函数是让父进程一直产生子进程,如果颠倒,多个子进程会一直抢夺监听,造成混乱,其次让父进程和多个客户端进行通信当奔溃时服务器将无法工作。如果是父进程监听,子进程进行创建连接的话,其实子进程崩溃,父进程仍然正常运行。


最后的最后,当子进程结束,我们需要对子进程的资源进行回收:

子进程退出时,内核会向父进程发送 SIGCHLD 信号。sigchld_handler 是一个信号处理函数,用于捕捉 SIGCHLD 信号,并调用 waitpid() 非阻塞地回收所有退出的子进程。把下面的代码加入到文件中。


void sigchld_handler(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc ,char* argv[]){
    signal(SIGCHLD, sigchld_handler);
    return 0;
}

文章的最后是服务器端的完整代码:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define Port 8888
#define BACKLOG 2
void sigchld_handler(int signo) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc ,char* argv[]){
    int ss,sc;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;

    int err;
    pid_t pid;
    signal(SIGCHLD, sigchld_handler);

    ss = socket(AF_INET,SOCK_STREAM,0);
    if(ss < 0){

        printf("socket error \n");
        return -1;
    }
    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(Port);

    err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
    if(err<0){
        printf("bind error \n");
        return -1;
    }

    err = listen(ss,BACKLOG);
    if(err<0){

        printf("listen error \n");
        return -1;
    }
    for(;;){
        socklen_t addrlen = sizeof(struct sockaddr);
        sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);

        if(sc<0){
            continue;
        }

        pid = fork();

        if(pid == 0){
            close(ss);

            char buffer [1024] = {0};
            char message [100] = {0};
            ssize_t size;
            for(;;){

                memset(buffer, 0, sizeof(buffer));

                size = read(sc,message,sizeof(message));
                if(size == 0){
                    return 0;
                }
                sprintf(buffer,"server have received %s \n",message);
                //write(sc,buffer,sizeof(buffer));
                write(1,buffer,strlen(buffer));
            }
            close(sc);

        }else if(pid > 0){
            close(sc);
        }else{
            printf("fork error \n");
        }

    }

    return 0;
}

希望文章对你有所帮助,如有错误欢迎指出,下篇文章,我们将编写客户端,并且实现服务器端和客户端之间的通信。


http://www.kler.cn/a/457676.html

相关文章:

  • node内置模块之---path 模块
  • C++编程库与框架实战——ZeroMQ消息队列
  • 第二十六天 RNN在NLP中的应用
  • MarkDown怎么转pdf;Mark Text怎么使用;
  • 感知机参数更新策略
  • HTTP Scheme 通常指的是在 URL 中用于指定使用 HTTP 协议的方案(scheme)
  • Upload-labs 靶场(学习)
  • 【Linux】Socket编程-UDP构建自己的C++服务器
  • 3.微服务灰度发布落地实践(组件灰度增强)
  • AI 自动化编程的现状与局限
  • delete,drop,truncate的区别
  • ChatGPT与Postman协作完成接口测试(四)
  • sql注入杂谈(一)--union select
  • Mysql(MGR)和ProxySQL搭建部署-Kubernetes版本
  • 【机器学习篇】穿越数字迷雾:机器深度学习的智慧领航
  • 【Hackthebox 中英 Write-Up】Manipulating a CRUD API | 操控 CRUD API:一步步提取 Flag
  • 一个线程中总共3个串行任务,在另一个线程中展示任务进行的实施进度。
  • XXL-TOOL v1.3.2 发布 | Java工具类库
  • 【10】Selenium+Python UI自动化测试 邮件发送测试报告(某积载系统实例-04)
  • Mac 安装Mysql启动Mysql以及数据库的常规操作
  • Python 中常见的一些画图形式
  • driftingblues6_vh靶机
  • 开源 AI 智能名片商城小程序:个人 IP 运营赋能商业腾飞
  • 计算机网络:TCP/IP网络协议
  • 【代码随想录|完全背包问题】
  • interceptor 和异常全局处理 Advice Advice中没有捕获异常