如何使用创建的套接字收发数据

简介: 【4月更文挑战第2天】发送成功仅表明数据进入发送缓冲区,实际传输和接收端接收是后续过程。

连接建立的根本目的是为了数据的收发。

1、发送数据

发送数据时常用的有三个函数,分别是 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 写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。

2、发送缓冲区

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


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


这里有几种情况:

第一种情况很简单,操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,那么皆大欢喜,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。


第二种情况是,操作系统内核的发送缓冲区是够大了,不过还有数据没有发送完,或者数据发送完了,但是操作系统内核的发送缓冲区不足以容纳应用程序数据,在这种情况下,你预料的结果是什么呢?报错?还是直接返回?


操作系统内核并不会返回,也不会报错,而是应用程序被阻塞,也就是说应用程序在 write 函数调用处停留,不直接返回。术语“挂起”也表达了相同的意思,不过“挂起”是从操作系统内核角度来说的。


实际上,每个操作系统内核的处理是不同的。大部分 UNIX 系统的做法是一直等到可以把应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。怎么理解呢?


当 TCP 连接建立之后,它就开始运作起来。你可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据),这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包,最后走数据链路层将数据发送出去。这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。注意返回的时刻,应用程序数据并没有全部被发送出去,发送缓冲区里还有部分数据,这部分数据会在稍后由操作系统内核通过网络发送出去。

image.png

3、读取数据

套接字描述本身和本地文件描述符并无区别,在 UNIX 的世界里万物都是文件,这就意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。这些函数包括 read 和 write 交换数据的函数。


最简单的 read 函数的原型如下:

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

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


4、服务器端读取数据程序

下面是服务器端读取数据的程序:

#include "lib/common.h"
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);          /* 关闭连接套接字,注意不是监听套接字*/
    }
}

对服务器端程序解释如下:

  • 21-35 行先后创建了 socket 套接字,bind 到对应地址和端口,并开始调用 listen 接口监听;
  • 38-42 行循环等待连接,通过 accept 获取实际的连接,并开始读取数据;
  • 8-15 行实际每次读取 1K 数据,之后休眠 1 秒,用来模拟服务器端处理时延。


5、客户端发送数据程序

下面是客户端发送数据的程序:

#include "lib/common.h"
#define MESSAGE_SIZE 102400
void send_data(int sockfd) {
    char *query;
    query = 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) {
            error(1, errno, "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)
        error(1, 0, "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) {
        error(1, errno, "connect failed ");
    }
    send_data(sockfd);
    exit(0);
}

对客户端程序解释如下:

  • 31-37 行先后创建了 socket 套接字,调用 connect 向对应服务器端发起连接请求
  • 43 行在连接建立成功后,调用 send_data 发送数据
  • 6-11 行初始化了一个长度为 MESSAGE_SIZE 的字符串流
  • 16-25 行调用 send 函数将 MESSAGE_SIZE 长度的字符串流发送出去


发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。

相关文章
|
8月前
|
网络协议 Python
Python网络编程基础(Socket编程)创建UDP socket对象
【4月更文挑战第8天】在Python中创建UDP服务器涉及使用`socket`模块创建socket对象,如`udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)`,然后绑定到特定IP地址和端口,如`udp_socket.bind((&#39;localhost&#39;, 12345))`。服务器通过`recvfrom`在无限循环中监听和接收数据报。这只是基础,实际应用还需处理接收、解析、响应及错误处理等。接下来可学习如何利用socket对象进行数据交互以构建完整服务器。
|
5月前
|
网络协议 数据格式 Python
python Socket无限发送接收数据方式
Socket是指套接字,是对网络中不同主机上的应用进程之间进行双向通信的端点的一种抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
|
8月前
|
存储 Python
Python网络编程基础(Socket编程)接收和发送数据
【4月更文挑战第9天】在UDP服务器编程中,我们已经创建了一个UDP套接字并绑定了地址和端口。接下来,服务器需要能够接收来自客户端的数据,并能够对这些数据进行处理和响应。下面,我们将详细讲解如何在UDP服务器中接收和发送数据。
|
网络协议 Python
Python 技术篇-socket套接字实现服务器客户端消息传递实例演示,UDP实现
Python 技术篇-socket套接字实现服务器客户端消息传递实例演示,UDP实现
274 0
Python 技术篇-socket套接字实现服务器客户端消息传递实例演示,UDP实现
|
网络协议 Python
Python 技术篇-socket套接字实现两个窗口间消息传递实例演示,TCP实现
Python 技术篇-socket套接字实现两个窗口间消息传递实例演示,TCP实现
295 0
Python 技术篇-socket套接字实现两个窗口间消息传递实例演示,TCP实现
Socket传输结构体数据注意事项
【1 背景】 在Socket通信中,要传输结构化的数据或者要进行协议数据传输的时候,发送端必须要构造结构体进行数据传输。 接收端也必须通过同样的结构体进行解析。 但Socket传输结构体数据时候,稍有不慎就会出现:1)解析数据出错;2)接收数据不完整;3)解析为乱码等的Bug。
291 0
Socket传输结构体数据注意事项
|
存储 缓存 数据可视化
可视化文件消息收发一体化Socket实现V0.1
本设计旨在通过socket的TCP实现不同类型文件的收发,UDP实现消息的收发。 1.1 需求概述 基本需求: 1) 支持发送不同类型的文件(.txt,.doc,.jpg,.exe,.mp3等所有类型) 2) 支持发送字符消息。 3) 支持可视化、直观显示与操作。 4) 支持单机收发、局域网内两台机器收发。
131 0
可视化文件消息收发一体化Socket实现V0.1
|
网络协议 Python
Python socket 实现服务端和客户端数据传输(TCP)
服务器端 import socket #创建一个socket对象 socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = &quot;127.
udp客户端收发数据流程
1、创建客户端socket开始进行通讯。2、这时服务端应该先启动,并在知道服务端的ip以及端口号的时候才能进行通讯。3、本地不需要绑定ip以及端口号,在用此套接字对象发送消息的时候会自动分配活动端口(1024-65535) 每次重启程序可能每次都不一样。
1322 0
|
缓存 网络协议
udp服务端收发数据流程
1、创建服务端的socket以便开始通讯。2、绑定ip以及端口号,这样客户端才能找到这个程序。3、因为本地网卡不止一个所以尽量不写死,一般用""空来表示所有本地网卡。4、接下来开始通过绑定的ip以及端口开始监听消息,设置最大接收1024字节消息,以防文件过大,占满网络缓存区。
1353 0