socket网络编程之二(回显程序实例)

在我的上一篇文章socket网络编程之一(TCP套接字API)中,介绍了ipv4的套接字数据结构和tcp套接字相关API,本篇文章,将利用上一篇介绍的API写一个服务器回显程序,加深对TCP套接字API的理解.源码地址

注意!github上的源码是我在学习UNIX网络编程的过程中,对书中的源码的实现,是一系列的源码,还在完善中,echoProgram文件夹中的代码和本篇博文是对应的,下载下来之后请先查看, README文件。代码中的一些公共函数,比如错误处理和包裹函数我都是放在public文件夹下面的。包裹函数就是一些基本函数包含了错误处理操作,大家一看就能明白。

注意!!!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!

客户端代码

 int sockfd;
 struct sockaddr_in sockaddr;

 sockfd = Socket(AF_INET, SOCK_STREAM, 0);
 bzero(&sockaddr, sizeof(sockaddr));
 sockaddr.sin_port = htons(9999);
 sockaddr.sin_family = AF_INET;
 inet_pton(AF_INET,"127.0.0.1",&sockaddr.sin_addr);
    
 Connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
 str_cli(stdin, socked);
  • 上述代码,首先声明了一个套接字描述符和一个IPV4的套接字结构,然后调用Socket函数,赋值给sockfd.
  • bzero表示将sockaddr的值设置为0,在使用sockaddr之前,必须要调用bzero.
  • 设置服务端的端口号为9999,htons表示将主机子节序转换为网络子节序。设置sin_family的协议族为AF_INET.将一个点分十进制的地址,转换为sockaddr.sin_addr结构的地址。这样我们就完成了要连接到服务端的套接字的配置.
  • 调用connection函数,连接服务器,如果connect函数,成功返回,则表示TCP三次握手完成。完成之后,就可以进行通信了.connect函数,如果不返回,程序则会阻塞在connect调用上.
  • 最后我们调用,str_cli函数,和服务端进行通信,下面讲解str_cli函数.
void str_cli(FILE *fd,int sockfd){
    char sendline[MAXLINE],recvline[MAXLINE];
    ssize_t status;
    while ( Fgets(sendline, MAXLINE, fd)!=NULL  ) {
        Writen(sockfd, sendline, strlen(sendline));
        status = read(sockfd, recvline, MAXLINE);
        if (  status< 0  ) {
            err_sys("read error");
        }
        puts(recvline);
    }
}
  • 我们从while循环讲起,在while中首先调用fgets函数,等待用户从标准输入,如果用户一直没有输入,程序会是一只阻塞的,如果用户输入了一行,以回车键结束,fget函数解除阻塞,然后返回。
  • 如果fget函数接收到了输入,输入的数据存在sendline数组中,调用write函数会将数据发送出去,此时write函数阻塞,当发送完成之后,则解除阻塞,函数返回.
  • 如果数据发送成功,则调用read函数,用于接收服务端发送来的数据,此时程序依然阻塞,如果接收完数据了,就将数据打印出来。

自此,str_cli函数完成了,在正常情况下,该程序没有任何问题,但是有一种例外情况,就是,当程序阻塞在fgets函数期间,服务端程序崩溃了,服务端程序会发送关闭连接的请求,而此时,客户端程序是不知道的。然后用户输入文本,fgets解除阻塞,再调用write函数,由于服务端已经关闭了连接,write函数肯定会写入不成功,这样照成的问题是客户端由于阻塞在fgets上,不能及时知道服务端的状况,这样写出来的程序就有问题.因此我们考虑用selece函数,来解决该问题,下面是str_cli的select版本.

 void str_cli(FILE *fd,int sockfd){
    int maxfdp1;
    fd_set rset;
    char sendline[MAXLINE],recvline[MAXLINE];
    ssize_t readlen;
    
    //将fd_set全部设置为0
    FD_ZERO(&rset);
    
    for (; ; ) {
        //FD_SET表示我们关心的文件描述符
        FD_SET(fileno(fd),&rset);
        FD_SET(sockfd,&rset);
        //将maxfdp1设置为描述符+1是因为文件描述符是从0开始的
        maxfdp1 = ((int)fmaxf(fileno(fd), sockfd)) + 1;
        Select(maxfdp1, &rset, NULL, NULL, NULL);
        
        //FD_ISSET如果返回真,表示sockfd有数据了
        if ( FD_ISSET(sockfd,&rset) ) {
            readlen = read(sockfd, recvline, MAXLINE);
            //如果读取到的数据为0表示服务端的子进程被杀死了
            if ( readlen==0 ) {
                err_quit("server terminal");
            }
            if ( readlen < 0 ) {
                err_sys("read error");
            }
            
            //将输出的文件打印出来
            puts(recvline);
        }
        
        if ( FD_ISSET(fileno(fd),&rset) ) {
            if ( Fgets(sendline, MAXLINE, fd)==NULL ) {
                return;
            }
            
            Writen(sockfd, sendline, MAXLINE);
        }
    }
}
     
  • 首先调用FD_ZERO函数,将rset设置为0,我们在用fd_set结构的时候,必须要调用该函数.
  • 在for循环中,我们调用FD_SET函数,设置我们要关心的描述符.
  • 然后调用select函数,进行I\O复用,注意select函数,传递描述符的时候,一定是最大描述符+1,select的最后一个参数,是等待的时间,在等待的时间内,程序是阻塞的。传入NULL表示无限等待.
  • FD_ISSET用于判断到底是哪一个描述符的被激活了,如果是sockfd则调用读,若服务端发送了关闭连接,也能马上监测到了。

我们用select 函数可以解决,当fgets函数出于阻塞状态时,服务端关闭了连接,客户程序不能立马知道的情况。

服务端代码

    int listenfd,connfd;
    pid_t childPid;
    socklen_t clilen;
    struct sockaddr_in cliaddr,seraddr;
    
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_port = htons(9999);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_family = AF_INET;
    
    Bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    
    Listen(listenfd,LISTENQ);
    
    Sigal(SIGCHLD, sig_child);
    
    for ( ; ; ) {
        clilen = sizeof(cliaddr);
        connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
      
        //等于0表示子进程
        if ( ( childPid = Fork() ) == 0 ) {
            printf("子进程号为:%d",getpid());
            Close( listenfd );
            str_echo(connfd);
            exit(0);
        }
        
        Close(connfd); /* 父进程应当关闭连接 */
    }
  • 服务端程序,首先调用bind函数设置服务端的端口号,ip地址设置为INADDR_ANY表示通配IP地址。
  • 然后调用listen函数用于监听,listen函数在上一篇文章中有介绍,不多说.
  • 调用signal函数,处理信号,要用该函数的原因是,当子进程结束的时候,内核会给程序发送一个中断信号,告诉程序,他的子进程已经终止,我们要捕捉该信号,将子进程的资源回收,不然会照成资源的浪费.
  • 在for循环中,accept函数,在返回之前,是一直阻塞的,该函数属于慢系统调用,慢系统调用的意思是有可能永远阻塞下去,此时如果子进程终止,如果没有信号处理函数的话,会返回EINTR的错误。accept返回之后,表明三次握手完成。
  • 我们的每一个连接,都用一个子进程来处理,由于子进程是父进程的一份拷贝,因此我们要关闭一个套接字描述符,然后调用str_echo函数,用于处理客户端发来的数据,下面是str_echo函数.
void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];
    
again:
    while ( (n = read(sockfd, buf, MAXLINE)) > 0 ) {
        Writen(sockfd, buf, n);
    }
    
    if ( n < 0 && errno == EINTR )
        goto again;
    else if (n < 0)
        err_sys("str_echo: read error");
}
  • 当三次握手完成后,我们首先调用read函数,读取从客户端发来的数据,如果读取到了数据,则将数据原封不动的发回去。这便是程序的回显功能。在signal函数汇总,还有一个sig_child没有讲到.
void sig_child(int signo)
{
    pid_t pid;
    int stat;
    
    printf("当前进程号为:%d",getpid());
    
    printf("signal num = %d",signo);
    
    //pid = wait(&stat);
    //waitpid处理
    //printf("child %d terminated\n",pid);
    while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) {
        printf("child %d terminated\n",pid);
    }
}
  • 如果内核给进程发送信号,sig_child函数便是我们的信号处理函数的回调函数,调用waitpid函数,回收子进程。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,042评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,996评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,674评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,340评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,404评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,749评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,902评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,662评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,110评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,577评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,258评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,848评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,726评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,952评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,271评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,452评论 2 348

推荐阅读更多精彩内容