服务器开发之简单的TCP回射服务器(一):服务器程序

通过学习Unix网络编程卷一:套接字联网API,实现了一个完整的TCP客户/服务器程序示例,这个例子执行如下步骤构建了一个基本的回射服务器:
1. 客户从标准输入读入数据,并发送给服务器;
2. 服务器从网络输入读入数据,进行处理后回射给客户;
3. 客户从网络输入读入数据,并在标准输出显示。
首先是服务器程序:

#include <sys/socket.h>
#include <sys/types.h>    // 提供pid_t size_t ssize_t等类型的定义
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <sys/wait.h>     // 提供wait()函数的定义
#include <signal.h>       // 提供signal()函数的定义
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <iostream>

#include "sum.h"

#define SERV_PORT 9877    // 服务器端口
#define LISTENQ 1024      // listen()的第二个参数backlog值
#define MAXLINE 4096      // 最大文本行数
#define BUFFSIZE 8192     // 读写缓冲区大小

using namespace std;

// 处理每个客户的服务:从客户读入数据,并把它们回射给客户
void str_echo(int sockfd)
{
    // size_t和ssize_t都是用来提高程序的可移植性的
    // ssize_t是有符号整型,等同于int(32位机器)/long(64位机器)
    // size_t就是无符号型的ssize_t,也就是unsigned int(32位)/unsigned long(64位)
    // 用法区别:size_t一般用于缓冲区大小这种非负的场景,而对于像read/write等函数,可能
    //          失败返回负数的时候用ssize_t
    ssize_t m, n;
    char buf[MAXLINE]; //read缓冲区
again:
    // read函数从打开的设备或文件中读取数据
    // ssize_t read(int sockfd, void* buff, size_t n) 从sockfd中读n字节数据到buff中
    // 读取成功返回读取的字节数,失败返回-1并设置errno,如果调用read之前已达文件末尾,返回0
    // write函数向打开的设备或文件中写数据
    // ssize_t write(int sockfd, void* buff, size_t n) 向sockfd中写n字节数据到buff中
    // 写成功返回写入的字节数,失败返回-1并设置errno
    // 两个函数的头文件为 #include <unistd.h>

    // socket编程接口提供了几个专门用于socket数据读写的系统调用,用于TCP流数据读写的是:
    // #include <sys/types.h>
    // #include <sys/socket.h>
    // ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    // ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    // 参数和返回值情况跟read和write函数一样,flags通常设置为0。
    while((m = recv(sockfd, buf, MAXLINE, 0)) > 0)
    {
        cout <<"recv data: "<< buf << endl;
        // strlen函数在计数时遇到'\0'才停止,而接收到的数据没有'\0',需要在末尾加上'\0'
        buf[m] = '\0';
        n = send(sockfd, buf, strlen(buf), 0);

    }
    if(m < 0 && errno == EINTR)
        goto again;
    else if(m < 0)
        cout << "str_echo: read error" << endl;
    else if(errno == EINTR)
        cout << "recv/send error" << endl;
}

// 对两个数求和的处理函数(字符串转换)
void str_echo_sum(int sockfd)
{
    long arg1, arg2;
    ssize_t n;
    char buf[MAXLINE];
    for( ; ; )
    {
        if((n = recv(sockfd, buf, MAXLINE, 0)) == 0)
            return;
        buf[n] = '\0';
        if(sscanf(buf, "%ld%ld", &arg1, &arg2) == 2)
        {
            snprintf(buf, sizeof(buf), "%ld", arg1 + arg2);
        }
        else
            snprintf(buf, sizeof(buf), "input error\n");
        send(sockfd, buf, strlen(buf), 0);
    }
}

// 对两个数求和的处理函数(二进制字节流)
void str_echo_byte(int sockfd)
{
    ssize_t n;
    struct args args;
    struct result result;

    for( ; ; )
    {
        if((n = recv(sockfd, &args, sizeof(args), 0)) == 0)
            return;
        result.sum = args.arg1 + args.arg2;
        send(sockfd, &result, sizeof(result), 0);
        cout << args.arg1 <<  "***" << args.arg2;
    }
}
// SIGCHLD信号的处理函数,触发时用来处理僵尸进程
void sig_chld(int signo)
{
    pid_t pid;
    int stat;

    // wait 和 waitpid函数原型
    // #include <sys/wait.h>
    // pid_t wait(int *statloc);
    // pid_t waitpid(pid_t pid, int *statloc, int options);
    // 返回值:成功均返回(已终止子进程的)进程ID,出错返回0或-1(通过statloc指针返回子进程终止状态(int))

    // pid = wait(&stat);
    // 用waitpid不用wait因为可以通过WNOHANG选项告知waitpid在尚有未终止的子进程在运行时不要阻塞
    while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
        cout << "child " << pid << "terminated" << endl;
    // 警告:在信号处理函数中调用I/O函数是不合适的,此处是为了查看子进程的状态

    return;
}

//dup函数测试
void str_dup(int fd)
{
    close(STDOUT_FILENO);
    // dup/dup2函数用于复制文件描述符,可以实现把标准输入重定向到一个文件,
    // 或者把标准输出重定向到一个网络连接(比如CGI编程)。
    // #include <unistd.h>
    // int dup(int file_descriptor);
    // int dup2(int file_descriptor_one, int file_descriptor_two);
    // 函数成功返回系统当前可用的最小整数值,失败返回-1并设置errno。
    dup(fd);
    char words[MAXLINE];
        cout << "hello world!" <<endl;
}

int main(int argc, char* argv[])
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    // 创建socket:int socket(int domain, int type, int protocol);
    // domain参数标识底层协议族,参数值为PF_INET(用于ipv4)或PF_INET6(用于IPv6)。
    // type参数指定服务类型,服务类型(参数值)为SOCK_STREAM服务(流服务,表示使用TCP协议)
    // 和SOCK_UGRAM(数据报,表示使用UDP协议)服务。
    // protocol参数在前两个参数确定后再选择的一个具体的协议,几乎所有的情况都设置为0,表示使用默认协议。
    listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert( listenfd >= 0 );

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // INADDR_ANY为通配地址(inet_addr("0.0.0.0"))
    // 作用是当服务器有多个网卡(对应多个IP地址)的时候,接收所有发到服务器的数据,与IP无关。
    servaddr.sin_port = htons(SERV_PORT);

    // bind函数把一个本地协议地址赋予一个套接字
    // #include <sys/socket.h>
    // int bind(int sockfd, const struct sockaddr* address, sizeof(address));
    // 返回值:成功返回0,出错返回-1,
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    // listen函数用来监听套接字,仅为TCP调用。
    // #include <sys/socket.h>
    // int listen(int sockfd, int backlog);
    // 返回值:成功返回0,出错返回-1。
    // backlog包括处于SYN_RCVD状态的未完成连接和处于ESTABLISTENED状态的已完成连接,一般设为5。
    listen(listenfd, LISTENQ);

    // 俘获SIGCHLD信号,用来处理僵尸进程
    // 在listen调用之后调用此函数,并且要在fork第一个子进程之前完成,且只做一次!
    signal(SIGCHLD, sig_chld);
    //signal(SIGCHLD, SIG_IGN);
    // 一般把SIGCHLD信号的处理设定为SIG_IGN也是可行的,sig_child函数增加了可移植性

    for( ; ; )
    {
        // 服务器阻塞于accept调用,等待客户端连接的完成。
        clilen = sizeof(cliaddr);

        // 下面if语句的作用是重启被中断的系统调用,同样适用于read/write/select/open等调用
        if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen))< 0)
        {
            if(errno == EINTR)
                continue;
            else
                cout << "accept error" << endl;
        }

        // fork函数(包括有些系统提供的其变体)是Unix派生新进程的唯一方法。
        // #include <unistd.h>
        // pid_t fork(void);
        // 返回值:在父进程中返回子进程ID,子进程中返回0(子进程的父进程唯一,可以通过getppid取得父进程ID),出错返回-1。

        // fork为每一个客户派生一个处理他们的子进程。
        // fork产生子进程时,从父进程那里复制listen调用和accept调用。
        // 子进程关闭listen调用,处理跟客户的连接
        // 父进程关闭accept调用,可以在listen套接字上再次调用accept处理下一个客户连接。
        if((childpid = fork()) == 0)
        {
            close(listenfd);
            str_echo(connfd); // 基本的回射服务器,同时可用来测试select系统调用
            // str_echo_sum(connfd); // 从客户端接收字符串,返回long型和
            //str_echo_byte(connfd); // 从客户端接收字节流,返回连个数据和
            // str_dup(connfd); // 测试dup函数
            exit(0);
        }
        close(connfd);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值