一文搞懂socket读写数据接口API

30 篇文章 0 订阅
15 篇文章 1 订阅

对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了专门的几个socket读写数据的接口。

UDP数据读写函数

recvfrom(从另一个IP接收数据)

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

  • sockfd:之前创建的socket文件描述符,从这个文件描述符中读取数据
  • buf:用来存放接收数据的缓冲区
  • len:该缓冲区的大小,这里注意有坑,需要用sizeof(buf)-1,因为这里还需要一个空间的大小的来存放'\0'。
  • flags:一般设置为0,具体用法下面会讲到
  • src_addr:因为UDP没有通信连接的概念,因此每次接收数据都需要获取到发送端的socket地址(下面的sendto函数也是类似),src_addr就是发送端的源地址,对于服务器来说,这也就是客户端来发送的地址。从sockfd中读取到发送端的socket地址放到这个src_addr结构体中
  • addrlen:源地址结构体的大小,注意这里的类型是socklen_t*,需要&sizeof(src_addr),这也是一个输入输出型参数(关于输入输出型的参数意思,和上面的accept的第三个参数类似)
  • 返回值:失败返回-1,成功返回实际接收数据的大小。
【文章福利】小编推荐自己的Linux内核源码交流群:【 869634926】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!

sendto(发送数据给另一个IP)

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:之前创建的socket文件描述符
  • buf:要发送的数据在buf当中
  • len:这里和recvfrom不同的是,此处不需要sizeof(buf)-1,因为这里的buf数组可能不是满的,它的中间可能有'\0',sizeof(buf)得到的是整个缓冲区的大小,strlen(buf)得到的大小是到第一个'\0'的大小,所以此处需要用的是strlen(buf)
  • flags:一般设置为0,具体用法下面会讲到
  • dest_addr:指要把数据发送给哪个目的IP,如果是客户端给服务器发消息,那么dest_addr就是服务器的socket地址
  • addrlen:目的IP的大小,注意这里又有和recvfrom不同的地方,前面的addrlen的类型的是指针,这里不是指针,所以直接用sizeof(dest_addr)即可,因为这不是一个输入输出型参数
  • 返回值:失败返回-1,成功返回传送的数据的大小

注意:recvfrom函数和sendto函数还可以用于面向连接的socket读写,只需要把最后两个参数都设置为NULL就可以了,因为我们已经知道对端的socket地址了,这就有点像下面要将的recv和send了。

 

下面贴上UDP协议的客户端和服务器的代码

server.c

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
3.
#include <arpa/inet.h>
#include <stdlib.h>

//一个服务器程序的典型逻辑
//1.服务器的初始化和启动(指定IP地址和端口号,加载需要的数据文件)
//2.进入事件循环(死循环),无限等待客户需求
// a).读取客户端发送的数据
// b).根据客户端发送的数据进行计算(对于不同用途的服务器,计算的逻辑不同,其中的过程可能很复杂,涉及到几十台服务器之间的相互配合)
// c).根据计算出来的不同结果拼接相应的字符串

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

// ./server [ip][port]
// ./server 192.168.224.136 9090
int main(int argc,char *argv[]){
    //如果给的参数不是3个,报错返回
    if(argc != 3){
        printf("Usage ./server [ip][port]\n");
        return 1;
    }

    int fd = socket(AF_INET,SOCK_DGRAM,0);//创建socket文件描述符
    if(fd < 0){
        perror("socket");
        return 1;
    }

    //创建一个IPv4的网络编程结构体
    sockaddr_in addr;
    addr.sin_family = AF_INET;//sin_family指定为AF_INET,表示用的IPv4结构
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    //把下标为1的参数的字符串转化为in_addr的结构,in_addr用来表示一个IPv4的IP地址,其实就是一个32位整数(把点分十进制转化为32位整数)
    //在实际传参的时候,这个参数通常设为0,表示本地的任意IP地址
    //因为服务器这个机器上可能有多个网卡,每个网卡也可能绑定多个IP地址
    //这样设置可以在所有的IP地址上都能收到请求,直到与某个客户端建立了连接以后才确定接下来到底用哪个IP地址
    addr.sin_port = htons(atoi(argv[2]));
    //把下标为2的参数先用aoti从字符串转化为数字,然后用htons将16位短整数数字从主机字节序转化为网络字节序

    int ret = bind(fd,(sockaddr*)&addr,sizeof(addr));//将IP地址和端口号与文件描述符关联起来
    if(ret < 0){
        perror("bind");
        return 1;
    }
    
    //bind之后就可以直接通信了
    
    //对于服务器来说,程序要一直循环下去,需要一个while(1)死循环
    while(1){
        sockaddr_in client_addr;//创建一个client IPv4结构体
        socklen_t len=sizeof(client_addr);//用len记下client的长度,之后有用

        printf(">: ");
        fflush(stdout);
        char buf[1024]={0};
        ssize_t s = recvfrom(fd,buf,sizeof(buf)-1,0,(sockaddr*)&client_addr,&len);   
        //从client服务器中接收数据到buf中        
        //因为这里需要知道发送端的socket地址,所以需要创建一个client_addr,这里创建的是IPv4
        //从fd中读到的发送端的socker地址就放到client_addr这个结构体中
        //&len是一个输入输出型参数,输入的是client_addr结构体的大小,返回的是实际接收到的socket地址类型的结构体大小       
        if(s < 0){
            perror("recvfrom");
            continue;//这里不能退出,需要continue,不能让这个死循环停下
        }
        buf[s] = '\0';//s的大小表示接受了多少数据

        printf("[%s:%d]:%s\n",inet_ntoa(client.sin_addr),
        ntohs(client.sin_port),buf);
        //第一个参数是将客户端的IP地址,通过inet_ntoa函数转化为字符串输出来,
        //第二个参数代表的是通过ntohs函数,将客户端传过来的端口号把其网络字节序转化为主机字节序
        //第三个参数代表的是直接从客户端接收到buf数组当中的内容

        //接下来就是服务器接收到客户端请求后的相应,对于不同的服务器来说,要根据不同的请求(request),做出不同的相应(response)
        //此处由于我们只实现最简单的echo服务器,就不涉及复杂的计算了,直接相应客户端传给服务器的数据
        sendto(fd,buf,strlen(buf),0,(sockaddr*)&client_addr,sizeof(client_addr));
        //将客户端传过来存在buf中的数据原模原样发送给客户端
        //用strlen(buf)表示缓冲区实际存放的数据的大小
        //那么此时发送端的socket地址就是前面的client_addr
        //这里的sizeof(client_addr)就不是一个输入输出型参数了,因此它不是一个地址,是值传递
    }

    return 0;
}

client.c

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

int main(int argc,char *argv[])
{
    if(argc != 3){
        printf("Usage  ./client [ip][port]\n");
        return 1;
    }

    int fd = socket(AF_INET,SOCK_DGRAM,0);//创建一个socket网络通讯端口
    if(fd < 0){
        perror("socket");
        return 1;
    }

    //下面的几步都和服务器当中的意思相同
    //只不过要注意的是,我们这里的这个server_addr实际上服务器
    //在传参的时候,argv[1]传的应该是一个127.0.0.1这样的本地地址
    //argv[2]传的是服务器绑定的端口号
    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));
    
    while(1){
        char buf[1024]={0};
        printf("Please Enter# ");
        fflush(stdout);
        
        socklen_t len=sizeof(server_addr);
       
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        //从标准输入中读取数据到buf中
        if(read_size < 0){
            perror("read");
            return 1;
        }
          
        buf[read_size]='\0';
           
        ssize_t write_size = sendto(fd,buf,strlen(buf),0,(sockaddr*)&server_addr,sizeof(server_addr));
        //从buf中读取数据发给server_addr
        if(write_size < 0){
            perror("sendto");
            return 1;
        }

        char buf_recv[1024]={0};
        read_size = recvfrom(fd,buf_recv,sizeof(buf_recv)-1,0,(sockaddr*)&server_addr,&len);
        //接收从server_client传回来的数据    
        if(read_size < 0 ){
            perror("recvfrom");
            return 1;
        }
                
        buf_recv[read_size]='\0';
        printf("server echo# %s\n",buf);
        //输出buf中存放的服务器发来的数据
    }

    return 0;
}

执行结果

多个客户端同时请求也是可以的

client进程的端口号49052,40911,34589是系统自动分配的

关于两个函数的flags具体取值,有下面这些取法

选项名含义sendto(send)recvfrom(recv)
MSG_CONFIRM指示数据链路层协议持续监听对方的回应,知道得到对方的答复。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socketYN
MSG_DONTROUTE不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送发明确知道目标主机就在本地网络上YN
MSG_DONTWAIT对socket的此次操作将是非阻塞的YY
MSG_MORE告诉内核应用程序还有更过数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可防止TCP发送过多小的报文段,从而提高传输效率YN
MSG_WAITALL读操作仅在读取到指定数量的字节后才返回(就是s读到第三次参数指定的len大小后才返回)NY
MSG_PEEK窥探读缓冲中的数据,此次读操作不会导致这些数据被清除(只是看,不读出来)NY
MSG_OOB发送或接收紧急数据YY
MSG_NOSIGNAL往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号YN

TCP数据读写函数

recv(从socket中读取数据)

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd是之前创建的文件描述符,并且是已经命名过(bind)和设置监听了的(listen)
  • buf:用来存放接收数据的缓冲区
  • len:该缓冲区的大小,注意和recvfrom中的一样也要留一个空间给'\0'
  • flags:同recvfrom

send(往socket中写入数据)

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd是之前创建的文件描述符,并且是已经命名过(bind)和设置监听了的(listen)
  • buf:发送的是buf缓冲区中的数据
  • len:缓冲区实际的有效大小(字符串就用strlen)
  • flags:同sendto

下面贴上TCP协议的客户端和服务器的代码(这里用的是read和write,这只是个人习惯,都可以的)

server.c

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>


typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;


int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip][port]\n");
        return 1;
    }

    int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
    if(fd < 0){
        perror("socket");
        return 1;
    }

    sockaddr_in server_addr;
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr .s_addr=inet_addr(argv[1]);
    server_addr.sin_port=htons(atoi(argv[2]));

    int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
    if(ret < 0){
        perror("bind");
        close(fd);
        return 1;
    }

    ret = listen(fd,5);//开启监听
    if(ret < 0){
        perror("listen");
        close(fd);
        return 1;
    }

    printf("bind and listen success,wait accept....\n");

    while(1){
        sockaddr_in client_addr;//创建一个客户端IP
        socklen_t len = sizeof(client_addr);
        int client_sock = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的连接
        if(client_sock < 0){
            perror("accept");
            close(fd);
            continue;
        }
        
        printf("connected,ip is %s,port is %d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
        //连接成功之后执行下列执行流进行响应
        while(1){
            printf("> ");
            fflush(stdout);
            char buf[1024]={0};
            ssize_t read_size = recvfrom(client_sock,buf,sizeof(buf)-1,0,(sockaddr*)&client_addr,&len);
            if(read_size < 0){
                perror("read");
                close(fd);
                return 1;
            }
            
            buf[read_size]='\0';
            printf("client :# %s",buf);

            
            sendto(client_sock,buf,strlen(buf),0,(sockaddr*)&client_addr,len);

            printf("Please wait.....\n");
        }
    }

    close(fd);
    return 0;
}

client.c

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>


typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;


int main(int argc,char *argv[]){
    if(argc != 3){
        printf("Usage ./client [ip][port]\n");
        return 1;
    }

    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0){
        perror("socket");
        return 1;
    }

    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));

    int ret = connect(fd,(sockaddr*)&server_addr,sizeof(server_addr));
    if(ret < 0){
        perror("connect");
        close(fd);
        return 1;
    }

    printf("connect success......\n");

    while(1){
        printf("[client]:");
        fflush(stdout);

        char buf[1024]={0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0){
            perror("read");
            close(fd);
            return 1;
        }
        
        buf[read_size]='\0';

        ssize_t write_size =sendto(fd,buf,strlen(buf),0,(sockaddr*)&server_addr,sizeof(server_addr));
        if(write_size < 0){
            perror("sendto");
            close(fd);
            return 1;
        }

        char buf_recv[1024]={0};
        socklen_t len = sizeof(server_addr);
        read_size = recvfrom(fd,buf_recv,sizeof(buf_recv)-1,0,(sockaddr*)&server_addr,&len);
        if(read_size < 0){
            perror("recvfrom");
            close(fd);
            return 1;
        }

        buf_recv[read_size]='\0';
        printf("[server]: %s\n",buf_recv);
    }
    close(fd);
    return 0;
}

运行结果如下

注意: 再启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。 分析原因,是因为我们accept了一个请求之后,就在一直while循环尝试read,没有继续调用到accept,导致不能接受新的请求 所以这是不科学的,需要采用下面的多进程和多线程版本的TCP网络通信。

TCP网络程序(多进程版,client.c与上面的一样)

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;


void ProcessRequest(int client_fd,sockaddr_in* client_addr){
    while(1){
        char buf[1024]={0};
        ssize_t read_size = read(client_fd,buf,sizeof(buf)-1);
        if(read_size < 0){
            perror("read");
            continue;
        }
        else if(read_size == 0){
            //表示客户端输入完毕
            printf("[client] [%s] [%d] say bye!\n",
                    inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port));
            close(client_fd);
            break;
        }
        else{
            buf[read_size] = '\0';
            printf("[client] [%s] [%d] say %s\n",
                    inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port),buf);
            
            write(client_fd,buf,strlen(buf));
        }
    }
}

void CreateWorker(int client_fd,sockaddr_in* client_addr){
    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
        return ;
    }
    else if(pid == 0){
        //子进程
        if(fork() == 0){
            //孙子进程
            ProcessRequest(client_fd,client_addr);
        }
        exit(0);
    }
    else{
        //父进程
        close(client_fd);
        waitpid(pid,NULL,0);
    }
}

int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip][port]\n");
        return 1;
    }

    int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
    if(fd < 0){
        perror("socket");
        return 1;
    }

    sockaddr_in server_addr;
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr .s_addr=inet_addr(argv[1]);
    server_addr.sin_port=htons(atoi(argv[2]));

    int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
    if(ret < 0){
        perror("bind");
        close(fd);
        return 1;
    }

    ret = listen(fd,5);//开启监听
    if(ret < 0){
        perror("listen");
        close(fd);
        return 1;
    }

    printf("bind and listen success,wait accept....\n");

    while(1){
        sockaddr_in client_addr;//创建一个客户端IP
        socklen_t len = sizeof(client_addr);
        int client_fd = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的链接
        if(client_fd < 0){
            perror("accept");
            close(fd);
            continue;
        }

        CreateWorker(client_fd,&client_addr);       
    }

    close(fd);
    return 0;
}

这里接受请求时,先由父进程创建了一个子进程,再由子进程创建了一个孙子进程,由孙子进程完成与客户端的交互。子进程一旦创建完孙子进程就退出,此时父进程wait到了子进程,执行完剩下的代码并退出。而此时,孙子进程因为子进程已经退出了,所以成为了僵尸进程,由系统1号进程进行回收孙子进程。

TCP网络通信(多线程版)

#include <stdio.h>
#include <sys/socket.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;


void ProcessRequest(int client_fd,sockaddr_in* client_addr){
    while(1){
        char buf[1024]={0};
        ssize_t read_size = read(client_fd,buf,sizeof(buf)-1);
        if(read_size < 0){
            perror("read");
            continue;
        }
        else if(read_size == 0){
            //表示客户端输入完毕
            printf("[client] [%s] [%d] say bye!\n",inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port));
            close(client_fd);
            break;
        }
        else{
            buf[read_size] = '\0';
            printf("[client] [%s] [%d] say %s\n",inet_ntoa(client_addr->sin_addr),ntohs(client_addr->sin_port),buf);
            
            write(client_fd,buf,strlen(buf));
        }
    }
}

typedef struct Arg{
    sockaddr_in client_addr;
    int client_fd;
}Arg;

void* CreateWorker(void *ptr){
    Arg* arg = (Arg*)ptr;
    ProcessRequest(arg->client_fd,&arg->client_addr);
    free(arg);
    return NULL;
}

int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage ./server [ip][port]\n");
        return 1;
    }

    int fd=socket(AF_INET,SOCK_STREAM,0);//创建一个socket文件描述符,SOCK_STREAMBIAOSHI面向字节流
    if(fd < 0){
        perror("socket");
        return 1;
    }

    sockaddr_in server_addr;
    server_addr.sin_family=AF_INET;
    server_addr.sin_addr .s_addr=inet_addr(argv[1]);
    server_addr.sin_port=htons(atoi(argv[2]));

    int ret = bind(fd,(sockaddr*)&server_addr,sizeof(server_addr));
    if(ret < 0){
        perror("bind");
        close(fd);
        return 1;
    }

    ret = listen(fd,5);//开启监听
    if(ret < 0){
        perror("listen");
        close(fd);
        return 1;
    }

    printf("bind and listen success,wait accept....\n");

    while(1){
        sockaddr_in client_addr;//创建一个客户端IP
        socklen_t len = sizeof(client_addr);
        int client_fd = accept(fd,(sockaddr*)&client_addr,&len);//接收客户端的链接
        if(client_fd < 0){
            perror("accept");
            close(fd);
            continue;
        }

        pthread_t tid;
        Arg *arg=(Arg*)malloc(sizeof(Arg));
        arg->client_fd = client_fd;
        arg->client_addr = client_addr;
        pthread_create(&tid,NULL,CreateWorker,(void*)arg);
        pthread_detach(tid);
    }

    close(fd);
    return 0;
}

通用数据读写函数

socket编程接口还提供了一对通用的数据读写接口。它们不仅能用于TCP,也能用于UDP

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
  • sockfd参数指定被操作的目标socket
  • msg参数是msghdr结构体指针,该结构体定义如下
struct msghdr {
    void*         msg_name;       /* socket地址 */
    socklen_t     msg_namelen;    /* socket地址长度 */
    struct iovec* msg_iov;        /* 分散的内存块 */
    size_t        msg_iovlen;     /* 分散的内存块数量 */
    void*         msg_control;    /* 指向辅助数据的起始位置 */
    size_t        msg_controllen; /* 辅助数据的大小 */
    int           msg_flags;      /* 复制函数中的flags参数,并在调用过程中更新 */
};

struct iovec {                    
    void  *iov_base;              /* 内存起始地址 */
    size_t iov_len;               /* 这块内存的长度 */
};
  • msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被置为NULL
  • msg_namelen成员指定了msg_name成员所指的socket地址的长度
  • msg_iov成员指向一个iovec结构体,该结构体封装了一块内存的起始位置和长度
  • msg_iovlen指定这样的结构体有几个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这称为分散读;对于sengmsg而言,msg_ioven块分散内存中的数据将被一并发送,这称为集中写。
  • msg_control和msg_controllen用于辅助数据的传送
  • msg_flags成员无需设定,它会复制传入的flags参数。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值