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

来源:互联网 发布:linux搭建邮件服务器 编辑:程序博客网 时间:2024/06/05 09:42

通过学习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);    }}