TCP/IP网络编程(10) IO函数

在Linux下,一般使用read & write函数完成数据IO,因为Linux下的套接字,可视为文件,其操作方式与文件类似,当套接字分配之后,会为其分配对应的文件描述符。在Windows下,则需要使用recv & send函数完成数据IO

1. Linux下的recv & send 函数

Linux下的recv 和send函数与windows下的其实并无差别,其参数顺序,含义,使用方法完全相同,与实际区别不大:

#include <sys/socket.h>

ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);

/*
sockfd: 与数据传输对象连接的套接字文件描述符
buf:    待传输数据的缓冲区
nbytes: 待传输的字节数
flags:  传输数据时指定的可选项信息
*/
#include <sys/socket.h>

ssize_t recv(int sockfd, const void* buf, size_t nbytes, int flags);

/*
sockfd: 与数据接收对象连接的套接字文件描述符
buf:    待接收数据的缓冲区
nbytes: 待接收的字节数
flags:  接收数据时指定的可选项信息
*/

send()和recv()函数的最后一个参数表示数据收发的可选项,该选项可使用位或运算同时传递多个信息:

可选项含义sendrecv
MSG_OOB用于传输带外数据(out-of-band data)
MSG_PEEK验证输入缓冲中是否存在接收的数据
MSG_DONTROUTE数据传输过程中不参照路由表,在本地网络中寻找目的地
MSG_DONTWAIT调用IO函数时不阻塞,用于使用非阻塞IO
MSG_WAITALL防止函数返回,直到接收全部请求的字节数

1.1 MSG_OOB发送紧急消息:

MSG_OOB可用于创建特殊发送方法和特殊发送通道,发送带外数据紧急消息。使用MSG_OOB收发消息例程如下所示:

客户端client.cpp代码:

/*
    send the msg oob data
*/

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

#define BUFF_SIZE    30

#define RECV_ADDRESS    "127.0.0.1"
#define RECV_PORT       54100

void error_handler(char* message);


int main(int argc, char* argv[])
{
    int sock;
    struct sockaddr_in recvAddr;

    sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&recvAddr, 0, sizeof(recvAddr));
    recvAddr.sin_family = AF_INET;
    recvAddr.sin_addr.s_addr = inet_addr(RECV_ADDRESS);        // 地址
    recvAddr.sin_port = htons(RECV_PORT);                // 端口

    printf("Connecting to server......\n");

    if (connect(sock, (struct sockaddr*)&recvAddr, sizeof(recvAddr)) == -1)
    {
        char message[50];
        sprintf(message, "Failed to connect to address: %s  %d", RECV_ADDRESS, RECV_PORT);
        error_handler(message);
    }

    printf("Successfully connectiing server.\n");

    write(sock, "1234", strlen("1234"));
    send(sock, "1131", strlen("1131"), MSG_OOB);     // 紧急发送消息,带外数据 out of band (OOB)
    write(sock, "333", strlen("333"));
    send(sock, "991", strlen("991"), MSG_OOB);       // 紧急发送消息,带外数据 out of band (OOB)

    close(sock);

    return 0;
}

void error_handler(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

服务端server.cpp代码:

/* server receive data */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>

#define BUFF_SZIE   30
#define SERVER_ADDRESS    "127.0.0.1"
#define SERVER_PORT       54100

void error_handler(char* message);
void urge_handler(int signo);

int server_sock;
int client_sock;

int main(int argc, char* argv[])
{
    struct sockaddr_in serverAddr, clientAddr;
    
    int strLen, state;
    
    socklen_t addrSize = sizeof(serverAddr);

    struct sigaction act;

    char buf[BUFF_SZIE];
    memset(buf, 0, BUFF_SZIE);

    // 信号初始化
    act.sa_handler = urge_handler;       // 信号处理函数
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serverAddr.sin_port = htons(SERVER_PORT);

    if (bind(server_sock, (struct sockaddr*)&serverAddr, addrSize) == -1)
    {

        error_handler("Failed to bind()\n");
    }

    listen(server_sock, 5);

    client_sock = accept(server_sock, (struct sockaddr*)&serverAddr, &addrSize);

    if (client_sock == -1)
    {
        error_handler("Failed to accept new connection from client.\n");
    }

    fcntl(client_sock, F_SETOWN, getpid());

    state = sigaction(SIGURG, &act, 0);       // 注册信号

    while ((strLen = recv(client_sock, buf, BUFF_SZIE - 1, 0)) != 0)
    {
        /* code */
        if (strLen == -1)
            continue;

        char content[2*BUFF_SZIE];
        memset(content, 0, 2*BUFF_SZIE);

        sprintf(content, "Receive buffer is : %s\n", buf);
        puts(content);

        memset(buf, 0, BUFF_SZIE);
    }
    

    return 0;
}

void error_handler(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

/*
   如果客户端调用了紧急发送带外数据的函数,则系统会产生SIGURGE信号,并调用注册的信号处理函数进行处理
*/
void urge_handler(int signo)
{
    char buff[BUFF_SZIE];
    memset(buff, 0, BUFF_SZIE);

    int recvLen = recv(client_sock, buff, BUFF_SZIE - 1, MSG_OOB);

    printf("Urge Message: %s\n", buff);
}

输出:

需要介绍一下fcntl函数的作用:

fcntl函数可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性;

介绍:fcntl 函数 - 天池渔隐 - 博客园 (cnblogs.com)

    fcntl(client_sock, F_SETOWN, getpid());

上述语句含义表示:文件描述符client_sock指向的套接字引发的SIGURG信号处理进程将变为getpid函数返回值用作ID的进程。

上述描述中的处理SIGURG信号表示的是“调用SIGURG信号处理函数”。在多进程服务器中,多个进程可以共同拥有一个套接字文件描述符。例如,通过fork函数创建了子进程,并同时复制了文件描述符,此时如果发生了SIGURG信号,应该调用哪个进程的信号处理函数就成为了一个问题。因此,在处理信号的时候,必须指定哪一个进程来处理信号。在上述代码中,处理SIGURG信号的时候必须指定处理信号的进程,而getpid函数返回调用此函数的进程。则fctl语句指定的是当前进程作为处理SIGURG信号的主体。

*通过观察程序的输出结果,发现通过MSG_OOB可选项传递数据时,只会返回一个字节;通过MSG_OOB可选项传输数据时并不会加快数据的传输速度,而且通过信号处理函数读取传输过来的数据时,也只能读取到一个字节的数据,剩余的数据只能通未设置MSG_OOB可选项的普通输入函数进行读取。因为TCP不存在真正意义上的带外数据(Out of b band),真正意义上的带外数据需要通过单独的通信路径高速传输数据,TCP不提供这样的传输路径,仅是利用TCP的紧急模式进行传输。

检查输入缓冲区

同时设置MSG_PEEK和MSG_DONTWAIT选项,可以验证输入缓冲中是否存在接收的数据。设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲中的数据,也不会将缓冲中的数据进行删除。此选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读取数据是否存在。示例代码如下所示:

客户端代码:

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

#define  ADDRESS   "127.0.0.1"
#define  PORT      38400

int main(int argc, char** argv)
{
    int sock;
    struct sockaddr_in address;   

    sock  = socket(PF_INET, SOCK_STREAM, 0);

    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr(ADDRESS);
    address.sin_port = htons(PORT);

    if (connect(sock, (struct sockaddr*)&address, sizeof(address)))
    {
        printf("Failed to connect to server.\n");
        return -1;
    }

    write(sock, "3456", strlen("3456"));

    close(sock);

    return 0;
}


服务端代码:

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

#define  ADDRESS      "127.0.0.1"
#define  PORT         38400
#define  BUFF_SIZE    30

int main(int argc, char** argv)
{

    int serverSock, clientSock;

    struct sockaddr_in servAddr, clientAddr;

    socklen_t addrSize = sizeof(servAddr);

    char buffer[BUFF_SIZE];
    memset(buffer, 0, BUFF_SIZE);

    memset(&servAddr, 0, sizeof(servAddr));
    memset(&clientAddr,0 , sizeof(clientAddr));

    serverSock = socket(PF_INET, SOCK_STREAM, 0);

    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(ADDRESS);
    servAddr.sin_port = htons(PORT);

    int bindRes = bind(serverSock, (struct sockaddr*)&servAddr, sizeof(servAddr));

    if (bindRes == -1)
    {
        printf("Failed to bind the socket.");
        return -1;
    }

    // 开始监听
    listen(serverSock, 5);

    clientSock = accept(serverSock, (struct sockaddr*)&servAddr, &addrSize);

    while (true)
    {
        /* code */
        int recvLen = recv(clientSock, buffer, BUFF_SIZE-1, MSG_PEEK | MSG_DONTWAIT);

        if (recvLen > 0)
            break;
    }

    printf("Buffer content: %s\n", buffer);

    printf("Clearing the buffer.\n");

    memset(buffer, 0, BUFF_SIZE);

    recv(clientSock, buffer, BUFF_SIZE-1, 0);

    printf("Read again: %s\n", buffer);

    close(clientSock);
    close(serverSock);

    return 0;
}

运行结果:

 可以看到在第一次读取完缓冲区中的数据之后,并未将数据进行清除,还可再次读取数据

readv函数和writev函数 (矢量IO操作)

”这两个函数主要用于对数据进行整合传输以及发送“

即writev函数可以将分散保存在多个缓冲区中的数据一并发送,通过readv函数可以由多个缓冲区分别接收,因此适当使用这个两个函数可减少IO函数的调用次数。

#include <sys/uio.h>

ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);

filedes: 表示数据传输对象的套接字文件描述符,但是该函数并不只限于套接字,因此可以向read函数一样向其传输文件或标准输出描述符。

返回值:失败时返回-1,成功时返回发送的字节数

iov: iovec结构体数组的地址值,结构体iovec中包含待发送数据的位置和大小信息

iovcnt:  iovec结构体数组的长度

iovec结构体的声明如下所示:

struct iovec
{
    void* iov_base;     // 缓冲地址
    size_t iovlen;      // 缓冲大小
}

示例代码如下:

#include <stdio.h>
#include <sys/uio.h>

int main(int argc, char* argv[])
{
    struct iovec vec[2];

    char buf1[] = "sdkjfcdc";
    char buf2[] = "4613241356";

    vec[0].iov_base = buf1;
    vec[0].iov_len  = 3;

    vec[1].iov_base = buf2;
    vec[1].iov_len  = 5;

    int lengtn = writev(1, vec, 2);

    puts("");     // 输出一个换行

    printf("write length is: %d", lengtn);

    return 0;
}

输出结果:

readv函数的定义:

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);

// 返回值:失败时返回-1,成功时返回接受到的总字节数
filedes:传递接收数据的文件(套接字)描述符

示例代码:

#include <stdio.h>
#include <sys/uio.h>

#define BUFF_SIZE  100

int main(int argc, char* argv[])
{
    struct iovec vec[2];

    char buf1[BUFF_SIZE] = {0};
    char buf2[BUFF_SIZE] = {0};

    vec[0].iov_base = buf1;
    vec[0].iov_len  = 5;

    vec[1].iov_base = buf2;
    vec[1].iov_len  = BUFF_SIZE;

    int lengtn = readv(0, vec, 2);

    puts("");     // 输出一个换行

    printf("read length is: %d\n", lengtn);

    printf("First message is : %s\n", vec[0].iov_base);
    printf("Second message is : %s\n", vec[1].iov_base);
    printf("");

    return 0;
}

输出结果:

使用场景:需要传输的数据位于不同的缓冲区时,可以通过1次writev函数调用替代多次write函数调用,提高效率。同样,需要将输入缓冲区中的数据读入不同的位置时,也可以利用1次readv函数进行读取,从而提高效率。仅从C语言的角度来讲,减少函数的调用次数,也能相应的提高性能,但其更大的意义在于减少了数据包的个数,因此writev函数在不在Nagle算法时更有价值。

如果将不同位置数据按照发送的顺序,移动到一个大数组,再调用write一次发送,达到的效果与writev函数相同,但是很明显writev函数更为方便~

---------------------------------------end-------------------------------------------------

posted @ 2022-11-17 22:13  Alpha205  阅读(82)  评论(0编辑  收藏  举报