【网络编程】对套接字读写的理解(1):write、send 和read

在封装TCP读写的时候,遇到了读写函数的选择问题,进而研究了一下,在此记录。

一、发送数据

常用的三个函数分别为write、send 和 sendmsg。函数原型如下:

ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

每个函数可以单独使用,但使用的场景有所不同:

  • 第一个函数是常见的文件写函数名,将socketfd换成文件描述符,就是普通的文件写入。
  • 如果想指定选项,发送带外数据,就需要使用第二个带 flag 的函数。所谓带外数据,是一种基于 TCP 协议的紧急数据,用于客户端 - 服务器在特定场景下的紧急处理。
  • 如果想指定多重缓冲区传输数据,就需要使用第三个函数,以结构体 msghdr 的方式发送数据。

上面这些可能查下资料,大家基本都可以知道,然后觉得套接字读写和普通文件的读写没有什么区别,其实里面暗含了很多内在的区别。

对于普通文件描述符而言,一个文件描述符代表了打开的一个文件句柄,通过调用 write 函数,操作系统内核帮我们不断地往文件系统中写入字节流。注意,写入的字节流大小通常和输入参数 size 的值是相同的,否则表示出错。

对于套接字描述符而言,它代表了一个双向连接,在套接字描述符上调用 write写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。

下面就拿write函数举例,阐述发送缓冲区的概念。

发送缓冲区

首先建立一个概念,就是TCP三次握手成功之后,TCP连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区

发送缓冲区的大小可以通过套接字选项来改变,当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。

举例来说,客户端发送数据给服务端,客户端程序中调用write函数,得到了返回值(即写出去的字节数),只是把数据拷贝到了操作系统的发送缓冲区,对于服务端来说,可能并没有完全收到(测试的时候可以打印客户端的返回值,然后在服务端每次接受指定字节数,循环接收数据,可能需要客户端发送的数据要大一点,不然发出去,服务端已经全部收完了,看不出来)。

二、读取数据

在UNIX这个万物都市文件的世界,套接字描述符本身和文件描述并没有区别,这也就意味着,将套接字传递给那些原先为处理本地文件而设计的函数并没有什么不妥。下面来看一下read函数。

read函数

函数原型如下:

ssize_t read (int socketfd, void *buffer, size_t size)

read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。这里只讨论阻塞IO的情形。

注意这里是最多读取 size 个字节。如果我们想让应用程序每次都读到 size 个字节,就需要编写下面的函数,不断地循环读取。

/* 从socketfd描述字中读取"size"个字节. */
size_t readn(int fd, void *buffer, size_t size) {
    char *buffer_pointer = buffer;
    int length = size;

    while (length > 0) {
        int result = read(fd, buffer_pointer, length);

        if (result < 0) {
            if (errno == EINTR)
                continue;     /* 考虑非阻塞的情况,这里需要再次调用read */
            else
                return (-1);
        } else if (result == 0)
            break;                /* EOF(End of File)表示套接字关闭 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length);        /* 返回的是实际读取的字节数*/
}

三、缓冲区验证实验

下面提供了客户端和服务端的代码,根据个人测试结果,当客户端已经打印出已发送字节数时,服务端还在接受信息。

服务端(tcp_server.cpp)代码:

g++ tcp_server.cpp -o tcp_server
./tcp_server
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h> /* for bzero() */
#include <errno.h>
#include <time.h>
#include <unistd.h>

/* 从socketfd描述字中读取"size"个字节. */
size_t readn(int fd, void *buffer, size_t size)
{
    char *buffer_pointer = (char *)buffer;
    int length = size;

    while (length > 0)
    {
        int result = read(fd, buffer_pointer, length);

        if (result < 0)
        {
            if (errno == EINTR)
                continue; /* 考虑非阻塞的情况,这里需要再次调用read */
            else
                return (-1);
        }
        else if (result == 0)
            break; /* EOF(End of File)表示套接字关闭 */

        length -= result;
        buffer_pointer += result;
    }
    return (size - length); /* 返回的是实际读取的字节数*/
}

void read_data(int sockfd)
{
    ssize_t n;
    char buf[1024];

    int time = 0;
    for (;;)
    {
        fprintf(stdout, "block in read\n");
        if ((n = readn(sockfd, buf, 1024)) == 0)
            return;

        time++;
        fprintf(stdout, "1K read for %d \n", time);
        usleep(1000);
    }
}

int main(int argc, char **argv)
{
    int listenfd, connfd;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(12345);

    /* bind到本地地址,端口为12345 */
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    /* listen的backlog为1024 */
    listen(listenfd, 1024);

    /* 循环处理用户请求 */
    for (;;)
    {
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
        read_data(connfd); /* 读取数据 */
        close(connfd);     /* 关闭连接套接字,注意不是监听套接字*/
    }
}

客户端代码(tcp_client.cpp):

g++ tcp_client.cpp -o tcp_client
./tcp_client 127.1
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h> /* for bzero() */
#include <string.h>  /*for strlen() */
#include <errno.h>
#include <time.h>
#include <unistd.h>

#define MESSAGE_SIZE 102400

void send_data(int sockfd)
{
    char *query;
    query = (char *)malloc(MESSAGE_SIZE + 1);
    for (int i = 0; i < MESSAGE_SIZE; i++)
    {
        query[i] = 'a';
    }
    query[MESSAGE_SIZE] = '\0';

    const char *cp;
    cp = query;
    size_t remaining = strlen(query);
    while (remaining)
    {
        int n_written = send(sockfd, cp, remaining, 0);
        fprintf(stdout, "send into buffer %ld \n", n_written);
        if (n_written <= 0)
        {
            perror("send failed");
            return;
        }
        remaining -= n_written;
        cp += n_written;
    }

    return;
}

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    if (argc != 2)
        perror("usage: tcpclient <IPaddress>");

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(12345);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    int connect_rt = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    if (connect_rt < 0)
    {
        perror("connect failed ");
    }
    send_data(sockfd);
    exit(0);
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值