流式套接字客户端/服务器编程 (迭代服务器+并发服务器)

实现一个基本的流式套接字客户端/服务器通信程序,客户端和服务器按如下步骤交互:

(1)客户端向服务器发出日期时间请求字符串,如:%D %Y %A %T等。

(2)服务器从网络接收到日期时间请求字符串后,根据字符串格式生成对应的日期时间值返回给客户端。

//TCP服务器
/*
 用法:./server port
 说明:该流式套接字服务器程序工作于单进程模式,根据客户端发来的请求格式字符串,服务器回应对应的日期和时间
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFSIZE 1024
#define backlog 128 //等待队列大小


//stderr -- 标准错误输出设备(默认向屏幕输出)
static void bail(const char *on_what)
{
    fputs(strerror(errno),stderr);
    fputs(": ",stderr);
    fputs(on_what, stderr);
    fputc('\n',stderr);
    exit(1);
}
int main(int argc,char *argv[])
{
    int sockfd;//服务器套接字
    int new_fd;//服务器连接套接字
    struct sockaddr_in server_addr;//服务器监听套接字
    struct sockaddr_in client_addr;//客户端ip地址
    socklen_t size;
    
    
    int portnumber; //端口
    
    char reqBuf[BUFSIZE];//应用接收缓冲
    char dtfmt[BUFSIZE];//日期-时间结果字符串
    
    time_t td;//当前日期和时间
    struct tm tm;//日期时间结构体
    int z;
    if(argc!=2)
    {
        fprintf(stderr,"输入格式错误\n");
        exit(1);
    }
    if((portnumber=atoi(argv[1]))<0)
    {
        fprintf(stderr,"输入格式错误\n");
        exit(1);
    }
    //创建服务器监听套接字
    if ((sockfd=socket(PF_INET,SOCK_STREAM,0))==-1)
    {
        fprintf(stderr,"Socket error:%s\a\n",strerror(errno));//strerror是把出错信息打印到指定位置
        exit(1);
    }
    //为监听套接字准备ip地址和端口
    
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    server_addr.sin_port=htons(portnumber);
    //绑定套接字到指定地址和端口
    if ((bind(sockfd, (struct sockaddr*)(&server_addr),sizeof(server_addr)))==-1)
    {
        fprintf(stderr,"Bind error:%s\a\n",strerror(errno));
        exit(1);
    }
    
    //监听
    
    if (listen(sockfd,backlog)==-1)
    {
        fprintf(stderr,"Listen error:%s\a\n",strerror(errno));
        exit(1);
    }
    printf("waiting for the client's request...\n");
    //服务器主循环处理
    while (1)
    {
        size=sizeof(struct sockaddr_in);
        //接收一个客户端连接并创建服务器连接套接字
        if((new_fd=accept(sockfd, (struct sockaddr*)(&client_addr), &size))==-1)
        {
            fprintf(stderr,"Accept error:%s\a\n",strerror(errno));
            exit(1);
        }
        fprintf(stdout,"Server got connection from %s\n",inet_ntoa(client_addr.sin_addr));
        for (; ; )
        {
            //读取客户端发来的日期时间请求,若客户端没有发送请求,则服务器将阻塞
            z=read(new_fd, reqBuf, sizeof(reqBuf));
            if(z<0)
                bail("read()");
            //服务器检查客户端是否关闭了套接字,此时read操作返回0(EOF),如果客户端关闭了其套接字,则服务器将执行close结束此连接,然后开始接收下一个客户端的连接请求
            if(z==0)
            {
                close(new_fd);
                break;
            }
            
            //向请求谅解字符串尾添加NULL字符构成完整的请求日期时间字符串
            reqBuf[z]=0;
            
            
            //获取服务器当前日期和时间
            time(&td);
            tm=*localtime(&td);
            
            
            //根据请求日期字符串的格式串生成应答字符串,不清楚的可以查一下这个函数
            strftime(dtfmt, sizeof(dtfmt), reqBuf, &tm);
            
            
            
            //将格式化结果发送给客户端
            z=write(new_fd, dtfmt, strlen(dtfmt));
            if (z<0)
                bail("write()");
            
        }
    }
    
    return 0;
}


客户端:

//TCP客户端
/*
 
 用法:./client hostname port
 
 说明:本程序使用TCP连接和TCP服务器通信,当连接建立后,向服务器发送如下格式字符串
 
 格式字符串示例:
 (1) %D
 (2) %A %D %H:%M:%S
 (3) %A
 (4) %H:%M:%S
 (5)...
 
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFSIZE 1024
#define backlog 128 //等待队列大小
static void bail(const char *on_what)
{
    fputs(strerror(errno),stderr);
    fputs(": ",stderr);
    fputs(on_what, stderr);
    fputc('\n',stderr);
    exit(1);
}
int main(int argc,char *argv[])
{
    int sockfd;//客户端套接字
    char buf[BUFSIZE];
    struct sockaddr_in server_addr;
    struct hostent *host;
    int portnumber;
    int nbytes;
    int z;
    char reqBuf[BUFSIZE];
    if (argc!=3)
    {
        printf("输入格式错误\n");
        exit(1);
    }
    if ((host=gethostbyname(argv[1]))==NULL)
    {
        printf("输入格式错误\n");
        exit(1);
    }
    if ((portnumber=atoi(argv[2]))<0)
    {
        fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
        exit(1);
    }
    //创建客户端套接字
    
    if ((sockfd=socket(PF_INET,SOCK_STREAM,0))==-1)
    {
        fprintf(stderr,"Socket error:%s\a\n",strerror(errno));
        exit(1);
    }
    //创建服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(portnumber);
    server_addr.sin_addr=*((struct in_addr*)host->h_addr);
    
    //连接服务器
    if (connect(sockfd, (struct sockaddr*)(&server_addr),sizeof(server_addr))==-1)
    {
        fprintf(stderr,"connect error:%s\a\n",strerror(errno));
        exit(1);
    }
    printf("connected to server %s\n",inet_ntoa(server_addr.sin_addr));
    //客户端主循环输入 “quit”退出
    for (; ; )
    {
        //提示输入日期请求格式字符串
        
        fputs("\nEnter fotmat string(^D or 'quit' to exit):",stdout);
        if (!fgets(reqBuf,sizeof(reqBuf),stdin))
        {
            printf("\n");
            break;
        }
        //为日期时间请求字符串添加NULL字符作为结尾,另外同时去掉末尾的换行符
        
        z=strlen(reqBuf);
        if (z>0 && reqBuf[--z]=='\n')
            reqBuf[z]=0;
        
        
        if (z==0)//客户端仅键入Enter
            continue;
        
        
        //输入‘quit’退出
        if(!strcasecmp(reqBuf,"QUIT"))//忽略大小写比较
        {
            printf("press any key to end client.\n");
            getchar();
            break;
        }
        //发送日期时间请求字符串到服务器,注意请求信息中去掉了NULL字符
        
        z=write(sockfd, reqBuf, sizeof(reqBuf));
        printf("client has sent '%s' to the sever\n",reqBuf);
        if (z<0)
            bail("write()");
        
        
        
        //从客户端套接字中读取服务器发回的应答
        if ((nbytes=read(sockfd,buf,sizeof(buf)))==-1)
        {
            fprintf(stderr,"read error:%s\n",strerror(errno));
            exit(1);
        }
        //若服务器由于某种原因关闭了连接,则客户端需要处理此事件
        if(nbytes==0)
        {
            printf("server hs closed the socket.\n");
            printf("press any key to exit...\n");
            getchar();
            break;
        }
        buf[nbytes]='\0';
        
        
        //输出日期时间结果
        printf("result from %s port %u:\n\t'%s'\n",inet_ntoa(server_addr.sin_addr),(unsigned)ntohs(server_addr.sin_port),buf);
        
    }
    close(sockfd);
    return 0;
}





并发服务器:

Linux是一个多任务操作系统,可以允许多个程序同时运行,每个正在运行的程序构成了一个进程,可以利用Linux系统的多任务特性,通过创建子进程系统调用,让新产生的子进程对客户端请求进行后续处理,而主进程返回继续接收其它客户端发来的请求,这样就实现了同时对多个客户端请求的并行处理模式,其大概模式如下:


  1. pid_t new_pid;

    ...

    while(true)

    {

        accept();//接收一个客户端连接请求

        new_pid-fork();//创建用于处理此请求的子进程

        switch(new_pid)

        {

            case 0:

                for(;;)

                {

                    do_something();//处理此客户端请求

                    ...

                }

                break;

            case 1:  //出错

                ...

                break;

            default: //父进程

                ...

                break;

        }

    }

     


并发服务器端代码:

//TCP并发服务器
/*
 用法:./server port
 说明:该流式套接字服务器程序可工作于多进程模式,根据客户端发来的请求格式字符串,服务器回应对应的日期和时间
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <signal.h>
#define BUFSIZE 1024
#define backlog 128 //等待队列大小


//stderr -- 标准错误输出设备(默认向屏幕输出)
static void bail(const char *on_what)
{
    fputs(strerror(errno),stderr);
    fputs(": ",stderr);
    fputs(on_what, stderr);
    fputc('\n',stderr);
    exit(1);
}
//设定子进程退出信号处理函数
static void sigchld_handler(int signo)
{
    pid_t pid;
    int status;
    char msg[]="SIGCHLD caught!\n";
    write(STDOUT_FILENO, msg,sizeof(msg));
    //等待已退出的所有子进程
    do
    {
        pid=waitpid(-1, &status,WNOHANG);
    }while(pid>0);
    
}

int main(int argc,char *argv[])
{
    pid_t pid;
    int sockfd;//服务器套接字
    int new_fd;//服务器连接套接字
    struct sockaddr_in server_addr;//服务器监听套接字
    struct sockaddr_in client_addr;//客户端ip地址
    socklen_t size;
    
    
    int portnumber; //端口
    
    char reqBuf[BUFSIZE];//应用接收缓冲
    char dtfmt[BUFSIZE];//日期-时间结果字符串
    
    time_t td;//当前日期和时间
    struct tm tm;//日期时间结构体
    int z;
    struct sigaction child_action;
    
    if(argc!=2)
    {
        fprintf(stderr,"输入格式错误\n");
        exit(1);
    }
    if((portnumber=atoi(argv[1]))<0)
    {
        fprintf(stderr,"输入格式错误\n");
        exit(1);
    }
    //设置SIGCHLD信号处理函数
    memset(&child_action, 0, sizeof(child_action));
    sigemptyset(&child_action.sa_mask);  //清空信号集中的所有信号
    child_action.sa_flags|=SA_RESTART;   //由此信号中断的系统调用会自动重启
    child_action.sa_handler=sigchld_handler;//绑定到处理程序
    
    if(sigaction(SIGCHLD,&child_action,NULL)==-1)//设置SIGCHLD信号处理程序
    {
        perror("failed to ignore SIGCHLD");
    }
    
    
    //创建服务器监听套接字
    if ((sockfd=socket(PF_INET,SOCK_STREAM,0))==-1)
    {
        fprintf(stderr,"Socket error:%s\a\n",strerror(errno));//strerror是把出错信息打印到指定位置
        exit(1);
    }
    //为监听套接字准备ip地址和端口
    
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    server_addr.sin_port=htons(portnumber);
    //绑定套接字到指定地址和端口
    if ((bind(sockfd, (struct sockaddr*)(&server_addr),sizeof(server_addr)))==-1)
    {
        fprintf(stderr,"Bind error:%s\a\n",strerror(errno));
        exit(1);
    }
    
    //监听
    
    if (listen(sockfd,backlog)==-1)
    {
        fprintf(stderr,"Listen error:%s\a\n",strerror(errno));
        exit(1);
    }
    printf("waiting for the client's request...\n");
    
    
    //服务器主循环处理
    for(;;)
    {
        size=sizeof(struct sockaddr_in);
        
        
        //接收一个客户端连接并创建服务器连接套接字
        if((new_fd=accept(sockfd, (struct sockaddr*)(&client_addr), &size))==-1)
        {
            fprintf(stderr,"Accept error:%s\a\n",strerror(errno));
            exit(1);
        }
        fprintf(stdout,"Server got connection from %s\n",inet_ntoa(client_addr.sin_addr));
        pid=fork();
        switch (pid)
        {
            case -1:
                perror("fork failed");
                exit(1);
                break;
            case 0: //子进程
                puts("Entering the child\n");
                for (; ; )
                {
                    //读取客户端发来的日期时间请求,若客户端没有发送请求,则服务器将阻塞
                    z=read(new_fd, reqBuf, sizeof(reqBuf));
                    if(z<0)
                        bail("read()");
                    
                    
                    //服务器检查客户端是否关闭了套接字,此时read操作返回0(EOF),如果客户端关闭了其套接字,则服务器将执行close结束此连接,然后看是接收下一个客户端的连接请求
                    if(z==0)
                    {
                        close(new_fd);
                        break;
                    }
                    
                    //向请求谅解字符串尾添加NULL字符构成完整的请求日期时间字符串
                    reqBuf[z]=0;
                    
                    
                    //获取服务器当前日期和时间
                    time(&td);
                    tm=*localtime(&td);
                    
                    
                    //根据请求日期字符串的格式串生成应答字符串,不清楚的可以查一下这个函数
                    strftime(dtfmt, sizeof(dtfmt), reqBuf, &tm);
                    
                    
                    
                    //将格式化结果发送给客户端
                    z=write(new_fd, dtfmt, strlen(dtfmt));
                    if (z<0)
                        bail("write()");
                    
                }
                //结束此客户端处理后关闭连接套接字
                printf("Child process:%d exits.\n",getpid());
                close(new_fd);
                exit(0);
            default:
                //在父进程中,只需简单关闭对应的重复套接字文件描述符,因为子进程与父进程共享同一个文件描述符,\
                在子进程中文件也是打开的,父、子进程各自关闭它们不需使用的文件描述符,\
                这样就不会干扰对方使用的文件描述符
                puts("this is the parent\n");
                close(new_fd);
        }
        
        
    }
    
    return 0;
}


服务器程序设计中,fork函数调用引起的一个复杂问题是必须处理子进程的终止信号SIGCHLD,因为子进程结束时,将释放它所占有的大部分资源,而剩下的未释放资源仅当父进程获得子进程的终止信息后才予以释放,所以我们设置信号处理程序进行善后处理,避免出现僵尸进程。下面是子进程终止时父进程的相关善后处理:

(1)内核提交SIGHCLD信号,说明子进程已经终止。

(2)sigchld_handler信号处理函数开始执行,通过do...while()循环,调用waitpid直到没有任何已退出进程,另外waitpid使用WNOHANG参数(调用中waitpid发现没有已退出的子进程可收集,则返回0,不阻塞)因为一旦阻塞就无法为下一个可能到来的客服端连接请求服务。


最后,在父进程中要关闭连接套接字,这一步很重要,因为调用fork函数后,父进程和子进程都打开了相连的套接字,但是父进程并没有为此连接客户端进行具体服务,而是继续监听,所以它必须关掉,而子进程将继续使用此连接套接字为客户端提供后续服务。




  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值