socket05---recv && send使用,回射客户端

开始复习网络编程这一块,话说有一段时间了,那不多说了,开始吧!

这一节,我们学习的是recv和send函数的使用,如果对初始化socket,绑定,连接,write和read等基本操作不太熟悉的话,可以参考前几篇博文,还有网络字节序和本地字节序的转换,需要注意的点也不少,多写才能熟悉。

先介绍一下recv和send函数

recv函数
提供了和read一样的功能,不同的是它多了一个参数

ssize_t recv(int sockfd,void *buf,size_t len,int flags)

主要区别在第四个参数,前面的参数可以说是一样的

recv对应的flags有3个选项:
    MSG_PEEK:查看数据,并不从系统缓冲区移走数据
    MSG_WAITALL:等待所有数据,等到所有的信息到达时才返回,使用它时,recv返回一直阻塞,直到指定的条件满足时,或者发生错误
    MSG_OOB:接受或者发送带外数据

send函数
同理,和write函数的功能一样,主要区别在于第四个参数:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
第四个参数flags,有2个选项:
    MSG_DONTROUTE:不查找表,它告诉ip,目的主机在本地网络上,没必要查找表。(一般用在网络诊断和路由程序里面)
    MSG_OOB:接受或者发送带外数据  

总结一下:

这两个函数就是在基础的read和write上加上了第四个可供选择的参数,增加了一些扩展的功能。
下面我们需要使用到recv的PEEK属性来封装一个读取一整行的函数readline,在写readline之前把还需要使用到的自己封装的readn和writen函数,为了更好地理解read和write的工作原理,我们还是使用自己写的功能一样的readn和writen而不直接用read和write。

readn和writen函数

ssize_t readn(int fd,void *buf,size_t count)
{
    size_t nleft = count;
    ssize_t nread;

    char* bufp = (char*)buf;
    while(nleft > 0)
    {
        if((nread = read(fd,bufp,nleft)) < 0)
        {
            if(errno == EINTR)
                continue;
            return -1;
        }
        else if(nread == 0)
            return count-nleft;
        bufp += nread;          //指针偏移,一般来说是直接偏完count字节,没有出错的话
        nleft -= nread;         //这一步之前理解错了,只是使得跳出循环,而不是什么多次读取,一般我们这里一次读nleft
    }
    return count;
}


ssize_t writen(int fd,const void *buf,size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char* bufp = (char*)buf;

    while(nleft > 0)
    {
        if((nwritten = write(fd,bufp,nleft)) < 0)
        {
            if(errno == EINTR)
                continue;
            return -1;
        }
        else if(nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;       //同理,见上,仅仅是为了跳出循环,返回count
    }
    return count;

}

封装的recv_peek和readline函数

recv_peek

//recvread上增加了第四个参数,peek是不清空缓冲区
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
    while(1)
    {
        int ret = recv(sockfd,buf,len,MSG_PEEK);
        if(ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}
//这个函数的作用相当是先去瞟一眼缓冲区,看见有多少字符,返回这个字符数

readline

ssize_t readline(int sockfd,void *buf,size_t maxline)
{
    int ret;
    int nread;
    char* bufp = buf;   //指针指向buf缓冲区
    int nleft = maxline;
    while(1)
    {
        ret = recv_peek(sockfd,bufp,nleft);
        if(ret < 0)
            return ret;
        else if(ret == 0)
            return ret;      
//ret==0,当按下ctrl+d退出时候会发送一个信号终止,没写入字符时按下enter换行,会将换行符发送过去(这个发送过程后面会重点提到),下面for循环中的readn会读取换行符
        nread = ret;
        int i;
        for(i = 0;i<nread;i++)
        {
            if(bufp[i] == '\n')   //对等端必须将\n发送过来,不发送过来会导致程序卡死在这里
            //这里是识别\n,读取到换行符,也就是一整行了,读完就返回,没收到换行符号时继续下面的代码
            {
                ret = readn(sockfd,bufp,i+1);
                if(ret != i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }
        //一般来说程序正常不会走到这里(上面那个for循环中直接return了),我试过把下面这段代码注释掉,也是正常地发送信息
        //但是为了程序的严谨性,特殊情况也需要考虑到,当遇到接受数据量超过限制值的时候,这种情况比较特殊,单独拿出来解释下
        if(nread > nleft)
            exit(EXIT_FAILURE);
        nleft -= nread;
        ret = readn(sockfd,bufp,nread);  
        if(ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }
    return -1;
}

上面的这两段代码在server和client段均需要包含,因为接受和发送数据的函数调用是一样的
这时候,我们把server端的代码补全一下:

封装一个do_service(int sockfd)

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

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

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

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

main函数:暂时还是使用多进程的方式


int main(void)
{
    int listenfd; 
    if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) <0 )
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;   //ipv4的地址家族
    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)    
    {
        if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip地址是:%s \t 端口是:%d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

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

下面写客户端client.c:
直接写main函数

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"); 

    if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

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

    while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
    {
        writen(sock,sendbuf,strlen(sendbuf)); 
        //注意这里用的strlen,只读实际上写入的数据长度(\n字符也是一个有效字符,在没有到底容量限制的时候,
        //比如我发送了1023个字节+\n,最后一个字节\n会被截断成为\0)
        //这样会导致一系列的问题,下一篇博文单独讨论,这里先实现通信功能即可
        for(int i = 0;i < strlen(sendbuf);i++)
        {
            printf("%c\t",sendbuf[i]);
        }

        int ret = readline(sock,recvbuf,sizeof(recvbuf));

        if(ret == -1)
            ERR_EXIT("readline");
        else if(ret == 0)
        {
            printf("client close,原因可能关闭了.\n");
            break;
        }
        fputs(recvbuf,stdout);     //打印server回射过来的数据

        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
    close(sock);

    return 0;
}

最后给两个.c文件加上如下的头文件ERR_EXIT函数

#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)

最后就能正常通信了,一个回射多个客户端的server和多个client端,运行截图如下:
这里写图片描述

下一节讨论在本章代码中需要注意的几点常见的误区,和bug。

所有代码都经测试,可用,如有错误,还请指出,谢谢了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值