socket服务实例:TCP(面向流的socket)实现ECHO服务器与客户端

  • 所谓的ECHO服务就是在屏幕打印相关的参数,相关的应用过程如下:

服务器逻辑

  • 1.服务器启动之后创建服务器socket,进行相应的设置后始终调用accept(2)等待客户端的连入,客户端 正常连入之后创建一个子进程作为业务进程,对客户端进行服务,父进程 始终作为监听进程等待下一个客户端的连入;
  • 2.其中为了防止僵尸进程的出现,服务器需要有处理子进程退出的功能,简便起见,程序中直接安装一个信号处理程序,用于处理SIGCHLD信号,这个过程完全异步,没有体现在流程图内。
    如图为程序流程图
    socket服务实例:TCP(面向流的socket)实现ECHO服务器与客户端
    但是该服务器的逻辑比较简单,只能实现对一个客户端的单次连接,在连接后向客户端发出一定的信息,然后断开连接
    整理socket相关操作的函数原型:
int socket(int domain, int type, int protocol);//创建socket
//sock_fd = socket(AF_INET, SOCK_STREAM, 0);
//sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
int bind(int socket, const struct sockaddr *address, socklent address_len);//绑定地址端口,绑定 成功则  绑定失败则
int connect(int socket, const struct sockaddr *address, socklent address_len);//连接
//可以看出,其中的参数完全相同,
//bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))
int listen(int socket, int backlog);//监听模式 backlog 是指等待连接的队列长度,但是实际上的队列可能会大于这个数字,通常都取 5。
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);//接受连接

代码解析:

  • 1.服务器相关代码:
  • 创建socket:server_sock = socket(AF_INET, SOCK_STREAM, 0);
int server_sock,conn_sock;
server_sock =  socket(AF_INET,SOCK_STREAM,0);  //功能相关
if(server_sock<0){    //处理错误
	perror("socket(2) erros");
	goto create_error;
	//main内定义create_error内容
}
  • 2 绑定到端口:bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_add))
struct sockaddr_in server_addr,client_addr;
//IP 协议使用的地址描述数据结构,定义在头文件<netinet/in.h>中。
socklen_t sock_len  = sizeof(client_addr);
(void)memset(&server_addr,0,sock_len);//函数的功能是:将指针变量 s 所指向的前 n 字节的内存单元用一个“整数” c 替换,注意 c 是 int 型。s 是 void* 型的指针变量,所以它可以为任何类型的数据进行初始化。
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(LISTEN_PORT);
if(bind(server_sock,(struct sockaddr *)&server_addr,sizeof(server_add))){
	perror("bind(2) error");
	goto err;
}

绑定的地址和端口主要是在 57 到 59 行填充 sturct sockaddr_in 结构完成的,服务器没有
特殊要求的情况下,绑定地址用 INADDR_ANY 监听所有地址即可,另外要注意字节序的转
换,这对于程序,尤其是对可移植性有要求的程序是一定要注意的。

  • 设置为被动监听:
if(listen(server_sock,5)){
	perror("listen(2) error");
	goto err;
}
  • 接受新的连接:
while(true){
	sock_len = sizeof(client_addr);
	conn_sock = accept(server_sock,(struct sockaddr *)&client_addr,&sock_len);
	
	if (conn_sock < 0) {
            if (errno == EINTR) {
                /* restart accept(2) when EINTR */
                continue;
            }
            goto end;
        }
	printf("client from %s:%hu connected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        fflush(stdout);//清除所有的输入输出文件内容
  • 子进程连接客户端:
chld_pid	= fork();   //通过fork创建子进程
        if (chld_pid < 0) {//小于0则表明创建失败
            /* fork(2) error */
            perror("fork(2) error");
            close(conn_sock);
            goto err;
        } else if (chld_pid == 0){//为0则代表返回子进程
            /* child process */
            int ret_code;

            close(server_sock);
            ret_code	= tcp_echo(conn_sock);
            close(conn_sock);

            /* Is usage of inet_ntoa(2) right? why? */
            printf("client from %s:%hu disconnected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

            exit(ret_code); //exit(x)(x不为0)都表示异常退出  exit(0)表示正常退出   exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。
        } else {
            /* parent process */
            continue;
        }

执行调用之后,后面的代码
就分为父子两个进程继续运行。我们在这里让子进程去对新连接的客户端提供服务,服务完
成后就退出。父进程则继续进行监听,等待下一个客户端的连接。这样,服务器就可以并发
的对多个客户端进行服务响应。
子进程首先调用 close(2)关闭自己的监听 Socket,然后调用 tcp_echo()函数进行
服务。服务完成后关闭 Socket,打印客户端断开连接信息后退出进程。

  • 服务函数
int tcp_echo(int client_fd)
{
    char				buff[BUFF_SIZE]	= {0};
    ssize_t				len				= 0;

    len	= read(client_fd, buff, sizeof(buff));
    if (len < 1) {
        goto err;
    }

    (void)write(client_fd, buff, (size_t)len);

    return EXIT_SUCCESS;
 err:
    return EXIT_FAILURE;
    /*
    EXIT_SUCCESS是C语言头文件库中定义的一个符号常量。return EXIT_SUCCESS相当于return 0;return EXIT_FAILURE相当于return 1;  这样写有什么好处呢,程序有易读性吗?
    头文件stdlib.h中:#include <stdlib.h>
    Definition of the argument values for the exit() function 
    #define EXIT_SUCCESS 0
    #define EXIT_FAILURE 1 
    */
}
  • SIGCHLD信号处理函数,防止僵尸进程:
    所有的子进程在处理完毕服务之后,会直接调用 exit(3)终止自己。在这个
    时候,系统会保留他们返回的终止状态,并发送 SIGCHLD 信号给父进程,同时子进程进入
    僵尸态。只有父进程处理了之后资源才能真正地完全回收。所以可用如下这样一个函数来实
    现对僵尸子进程的回收处理。
void zombie_cleaning(int signo)
{
    int status;
    (void)signo;
    while (waitpid(-1, &status, WNOHANG) > 0);
}
  • 安装信号 处理函数
struct sigaction clean_zombie_act;
    (void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
    clean_zombie_act.sa_handler	= zombie_cleaning;
    if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
        perror("sigaction(2) error");
        goto err;
    }

客户端程序:

  • 启动后立即创建 Socket,并且直接调用 connect(2) 连接服务器,省去 bind(2)调用,系统会将刚才创建的 Socket 隐式绑定到一个随机端口上。connect(2)后直接发送数据到服务器,发送完毕后直接读取服务器回发的数据并打印接收到的数据,然后结束
    socket服务实例:TCP(面向流的socket)实现ECHO服务器与客户端
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>

#define SERVER_IP	"192.168.1.133"
#define SERVER_PORT	((uint16_t)7007)
#define BUFF_SIZE	(1024 * 4)

int main(int argc, char *argv[])
{
    int conn_sock;
    char test_str[BUFF_SIZE]	= "tcp echo test";
    struct sockaddr_in	server_addr;

    conn_sock	= socket(AF_INET, SOCK_STREAM, 0);
    if (conn_sock < 0) {
        perror("socket(2) error");
        goto create_err;
    }

    (void)memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family		= AF_INET;
    server_addr.sin_port		= htons(SERVER_PORT);
    if (argc != 3) {
        server_addr.sin_addr.s_addr	= inet_addr(SERVER_IP);//inet_addr()用来将参数cp 所指的网络地址字符串转换成网络所使用的二进制数字. 网络地址字符串是以数字和点组成的字符串, 例如:"163. 13. 132. 68".
    } else {
        server_addr.sin_addr.s_addr	= inet_addr(argv[1]);
        snprintf(test_str, BUFF_SIZE, "%s", argv[2]);
    }

    if (connect(conn_sock,
                (struct sockaddr *)&server_addr,
                sizeof(server_addr)) < 0) {
        perror("connect(2) error");
        goto err;
    }

    if (write(conn_sock, test_str, strlen(test_str)) < 0) {
        perror("send data error");
        goto err;
    }
    (void)memset(test_str, 0, BUFF_SIZE);
    if (read(conn_sock, test_str, BUFF_SIZE) < 0) {
        perror("receive data error");
        goto err;
    }
    printf("%s\n", test_str);
    fflush(stdout);//清空文件缓冲区或者标准输入输出缓冲区

    return EXIT_SUCCESS;

 err:
    close(conn_sock);
 create_err:
    fprintf(stderr, "client error");
    return EXIT_FAILURE;
}

服务器代码:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <signal.h>

#define LISTEN_PORT	((uint16_t)7007)
#define BUFF_SIZE	(1024 * 4)

void zombie_cleaning(int signo)
{
    int status;
    (void)signo;
    while (waitpid(-1, &status, WNOHANG) > 0);
}

int tcp_echo(int client_fd)
{
    char				buff[BUFF_SIZE]	= {0};
    ssize_t				len				= 0;

    len	= read(client_fd, buff, sizeof(buff));
    if (len < 1) {
        goto err;
    }

    (void)write(client_fd, buff, (size_t)len);

    return EXIT_SUCCESS;
 err:
    return EXIT_FAILURE;
    /*
    EXIT_SUCCESS是C语言头文件库中定义的一个符号常量。return EXIT_SUCCESS相当于return 0;return EXIT_FAILURE相当于return 1;  这样写有什么好处呢,程序有易读性吗?
    头文件stdlib.h中:#include <stdlib.h>
    Definition of the argument values for the exit() function 
    #define EXIT_SUCCESS 0
    #define EXIT_FAILURE 1 
    */
}

int main(void)
{
    int server_sock, conn_sock;  //连接的定义
    struct sockaddr_in server_addr, client_addr;
    socklen_t	sock_len	= sizeof(client_addr);
    pid_t	chld_pid;
    struct sigaction clean_zombie_act;

    server_sock	= socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("socket(2) error");  //C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。
        goto create_err;
    }

    (void)memset(&server_addr, 0, sock_len);
    server_addr.sin_family		= AF_INET;
    server_addr.sin_addr.s_addr	= htonl(INADDR_ANY);
    server_addr.sin_port		= htons(LISTEN_PORT);

    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
        perror("bind(2) error");
        goto err;
    }

    if (listen(server_sock, 5)) {
        perror("listen(2) error");
        goto err;
    }

    (void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
    clean_zombie_act.sa_handler	= zombie_cleaning;
    if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
        perror("sigaction(2) error");
        goto err;
    }

    while (true) {
        sock_len	= sizeof(client_addr);
        conn_sock	= accept(server_sock, (struct sockaddr *)&client_addr, &sock_len);
        if (conn_sock < 0) {
            if (errno == EINTR) {
                /* restart accept(2) when EINTR */
                continue;
            }
            goto end;
        }

        printf("client from %s:%hu connected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        fflush(stdout);

        chld_pid	= fork();   //通过fork创建子进程
        if (chld_pid < 0) {//小于0则表明创建失败
            /* fork(2) error */
            perror("fork(2) error");
            close(conn_sock);
            goto err;
        } else if (chld_pid == 0){//为0则代表返回子进程
            /* child process */
            int ret_code;

            close(server_sock);
            ret_code	= tcp_echo(conn_sock);
            close(conn_sock);

            /* Is usage of inet_ntoa(2) right? why? */
            printf("client from %s:%hu disconnected\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));

            exit(ret_code); //exit(x)(x不为0)都表示异常退出  exit(0)表示正常退出   exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。
        } else {
            /* parent process */
            continue;
        }
    }

 end:
    perror("exit with:");
    close(server_sock);
    return EXIT_SUCCESS;
 err:
    close(server_sock);
 create_err:
    fprintf(stderr, "server error");
    return EXIT_FAILURE;
}

上一篇:微服务*项目(08) - 监控架构设计Metrics


下一篇:Linux下的 Mysql 8.0 yum 安装 并修改密码