socket初接触---多用户服务器简单交互(下)

上一篇我们提到了很多的知识点,什么套接字,字节流,字节序….这次来简单实现一个服务器对多用户的简单消息映射交互(就是我在client端随便写段话传输给server,server接受后再返回给client端)

在写主代码之前,先看下面这个代码段:

#define ERR_EXIT(m) \
    do \
    { perror(m);  \
        exit(EXIT_FAILURE);\
    } while(0)

反斜杠(\ , 转义字符)表示换行符无效,下一行实际上属于上一行,上面的5行实际是一行,这么写为了好看一点.

这里插个段子,在查斜杠(/)和反斜杠(\)的时候,发现了一系列对某Mi..的吐槽,(在路径表示层级上)全世界都是用/,而它是用的\,这到也不是它非要与众不同,而是因为DOS系统源自CP/M系统(微机史上开篇的一个系统),这个系统不支持目录结构(导致DOS刚刚出来的时候也不支持目录结构),而同时/被用来表示命令行参数(相当与linux中的 - 和 –),之后升级了,支持目录结构之后,再用/就会导致很多冲突,要找一个最接近的符号来表示目录层级切换,看来看去也就“\”这兄弟最适合了。
如果说要改?40多年前改还行,而这已经成了历史性的问题,当初能改的时候没改,现在再改也没什么意义了,Mi其实已经做了很多友好的设置,你可以根据自己的喜好去更改使用/。

继续看上面的代码段:

#include<stdio.h>
//用来将上一个函数发生的错误的原因输出到标准输出

void perror(const char* s);

//参数s所指的字符串会先打印出,后面再加上错误原因字符串

//这个错误原因字符串根据全局变量errno决定,当你每次调用函数出错的时候,这个函数已经更改了errno的值。

//perror这个函数总而言之就是当你调用某个函数出错时将函数的名字(char* s)和对应的errno错误信息一起输出

关于exit,终结程序,跳回操作系统。只用exit(0)代表程序的正常退出,其余的参数都表示程序异常退出。(它被定义于stdio.h中)

最后一点就是关于do {} while(0);在代码中的效果,这里while(0)保证只执行上面的程序一次,并且是先do,所以必然会执行一次判断,之后退出这个函数。在一些c函数库或者Linux内核源码中经常能看见do {} while(0); 的使用.

终于到了我们的主函数了,首先先是我们的server.c,代码见下:

void do_service(int conn)
{
    char recvbuf[1024];
    int n;
    while(1)
    {
        memset(recvbuf,0,sizeof(recvbuf));

        int ret = read(conn,recvbuf,sizeof(recvbuf));

        if(ret == -1)
            ERR_EXIT("read");
        else if(ret == 0)    //ret==0表示啥都接收不到
        {
            printf("client close,原因可能是关闭了,也可能是连接断了\n");
            break;
        }
        fputs(recvbuf,stdout);  //向标准输出打印读到的recvbuf

        write(conn,recvbuf,sizeof(recvbuf));  //向客户端回射,说明server已经接收到你的信息了
    }
}

int main(void)
{
    int listenfd; //赋socket创建成功后的唯一表示符,描述字

    //创建监听套接字(等待client的请求)
    //通常第三个参数由前两个指定的参数决定,我们可以直接写0,表示让其自己判断
    if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) <0 )
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;   //ipv4的地址家族
    //清空servaddr的内存,从指定地址servaddr的开始,用 0 来替换指定多少(第三个参数)的内存大小

    memset(&servaddr , 0 , sizeof(servaddr)); 

    servaddr.sin_family = AF_INET; //暂时我们把AF_INET和PF_INET看成一样,细微的区别

    servaddr.sin_port = htons(5188);  //指定port为5188,并且将其转换为网络字节序,一个整形占2个字节,用s(hort)

    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示使用本机任意地址

    int on = 1;

    //解决关闭服务器立即重启时候的需要等待TIME_WAIT消失过程,除此之外,还有很多改善socket健壮性的选项,这里暂不讨论
    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    //绑定地址
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        ERR_EXIT("bind");

    //监听套接字
    if(listen(listenfd,SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);

    int conn;
    pid_t pid;

    while(1)    
    {
        //到这一步表明已经得到了一个已连接的socket描述字,之前的listenfd到这里不再进行操作了,但是在外层可以继续接受其他client的请求。接下来我们操作conn即好
        if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
            ERR_EXIT("accept");

        //连接上了就打印对面的ip地址和端口,注意转换字节序,ip是4字节,port是字节
        printf("ip地址是:%s \t 端口是:%d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

        //连接上了,之后需要对client作反馈操作,这里我们fork一个新的进程
        pid = fork();
        if(pid == -1)
            ERR_EXIT("fork");
        if(pid == 0) //for这个函数很特别(一次执行返回两个值,后续深入学习下这个函数),创建成功时它向子进程返回0
        {
            close(listenfd);    //关闭主进程的listenfd
            do_service(conn);  //操作连接的conn套接字,读取数据或是回射数据,见上
            exit(EXIT_SUCCESS); //跳出了上面的循环,那就意味着连接关闭了
        }
        else
            close(conn);   //fork返回给父进程的是pid在正真系统中的唯一值,在父进程中和conn无关,直接关闭
    }
        return 0;
}

到这里服务端就写完了,代码中写了很多的注释,不清楚的可以仔细看看,还有一些头文件要加在开头,讲完之后在下面附上源码。

下面就开始写client.c了,代码如下:

int main()
{
    //创建一个连接套接字
    int sock;

    if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);   //还是需要注意转换字节序,多提醒才不会忘

    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //使用server地址。单机版没办法啦,自己连自己
    //还有一种方式见下
    //inet_aton("127.0.0.1",&servaddr.sin_addr);

    //connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度
    if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};

    int n;

    //从键盘上读数进来
    while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
    {
        write(sock,sendbuf,strlen(sendbuf)); //注意这里用的strlen,只发送实际上写入的数据长度
        int ret = read(sock,recvbuf,sizeof(recvbuf));
        if(ret == -1)
            ERR_EXIT("read");
        else if(ret == 0)
        {
            printf("client close,原因可能关闭了.\n");
            break;
        }
        fputs(recvbuf,stdout);
        //清空一次数据
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
    close(sock); //上面那个循环都跳出来了那还sock个毛线,跳出的时候close

    return 0;
}

总结:
相对于server端,client端只需要创建一个用于connect(连接)的socket,然后struct 目标server的地址(ip+port),使用connect函数进行一个连接,之后使用write函数向sock发送数据,发送完了那就是server端的事情了,这里我们可以看出来,服务器那端fputs之后,就将接收到的信息再次回射给client了,这时候client端继续read函数,接收成功的话fputs,接下爱mnemset清空数据,继续下一次while( 1 )循环。
直到直到…..break
其他情况比如exit都是重试,下一次循环
这里注意:
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口

之后将这两个.c文件编译成可执行文件,执行即可进行通信,下面是执行图:
这里写图片描述

由于文件上传不了,大家在以上代码的基础上,可以将下面的代码粘在每份文件的头部,经过测试以上代码都是能直接使用的:

#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>

#include<stdio.h>
#include<stdlib.h>

#include<arpa/inet.h>
#include<netinet/in.h>

#include<errno.h>
#include<string.h>


#define ERR_EXIT(m) \
    do \
    { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值