套接字的作用与一般使用_05 | 使用套接字进行读写
连接建立的根本目的是为了数据的收发。拿我们常用的网购场景举例子,我们在浏览商品或者购买货品的时候,并不会察觉到网络连接的存在,但是我们可以真切感觉到数据在客户端和服务器端有效的传送, 比如浏览商品时商品信息的不断刷新,购买货品时显示购买成功的消息等。
首先来看一下发送数据
发送数据
发送数据时常用的有三个函数,分别是 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 函数,应该和在普通文件描述符上调用 write 函数的行为是一致的,都是通过描述符句柄写入指定的数据。
对于普通文件描述符而言,一个文件描述符代表了打开的一个文件句柄,通过调用 write 函数,操作系统内核帮我们不断地往文件系统中写入字节流。注意,写入的字节流大小通常和输入参数 size 的值是相同的,否则表示出错。
对于套接字描述符而言,它代表了一个双向连接,在套接字描述符上调用 write 写入的字节数有可能比请求的数量少,这在普通文件描述符情况下是不正常的。
产生这个现象的原因在于操作系统内核为读取和发送数据做了很多我们表面上看不到的工作。接下来我拿 write 函数举例,重点阐述发送缓冲区的概念。
发送缓冲区
你一定要建立一个概念,当 TCP 三次握手成功,TCP 连接成功建立后,操作系统内核会为每一个连接创建配套的基础设施,比如发送缓冲区。
发送缓冲区的大小可以通过套接字选项来改变,当我们的应用程序调用 write 函数时,实际所做的事情是把数据从应用程序中拷贝到操作系统内核的发送缓冲区中,并不一定是把数据通过套接字写出去。
这里有几种情况:
第一种情况很简单,操作系统内核的发送缓冲区足够大,可以直接容纳这份数据,那么皆大欢喜,我们的程序从 write 调用中退出,返回写入的字节数就是应用程序的数据大小。
第二种情况是,操作系统内核的发送缓冲区是够大了,不过还有数据没有发送完,或者数据发送完了,但是操作系统内核的发送缓冲区不足以容纳应用程序数据,在这种情况下,你预料的结果是什么呢?报错?还是直接返回?
操作系统内核并不会返回,也不会报错,而是应用程序被阻塞,也就是说应用程序在 write 函数调用处停留,不直接返回。术语“挂起”也表达了相同的意思,不过“挂起”是从操作系统内核角度来说的。
那么什么时候才会返回呢?
别忘了,我们的操作系统内核是很聪明的,当 TCP 连接建立之后,它就开始运作起来。你可以把发送缓冲区想象成一条包裹流水线,有个聪明且忙碌的工人不断地从流水线上取出包裹(数据),这个工人会按照 TCP/IP 的语义,将取出的包裹(数据)封装成 TCP 的 MSS 包,以及 IP 的 MTU 包,最后走数据链路层将数据发送出去。这样我们的发送缓冲区就又空了一部分,于是又可以继续从应用程序搬一部分数据到发送缓冲区里,这样一直进行下去,到某一个时刻,应用程序的数据可以完全放置到发送缓冲区里。在这个时候,write 阻塞调用返回。注意返回的时刻,应用程序数据并没有全部被发送出去,发送缓冲区里还有部分数据,这部分数据会在稍后由操作系统内核通过网络发送出去。
读取数据
注意到,套接字描述本身和本地文件描述符并无区别,在 UNIX 的世界里万物都是文件,这就意味着可以将套接字描述符传递给那些原先为处理本地文件而设计的函数。这些函数包括 read 和 write 交换数据的函数。
read 函数
先从最简单的 read 函数开始看起,这个函数的原型如下:
ssize_t read (int socketfd, void *buffer, size_t size)
read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。当然,如果是非阻塞 I/O,情况会略有不同,在后面的提高篇中会重点讲述非阻塞 I/O 的特点。
注意这里是最多读取 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 */elsereturn (-1);} else if (result == 0)break; /* EOF(End of File)表示套接字关闭 */length -= result;buffer_pointer += result;}return (size - length); /* 返回的是实际读取的字节数*/
}
对这个程序稍微解释下:
- 6-19 行的循环条件表示的是,在没读满 size 个字节之前,一直都要循环下去。
- 10-11 行表示的是非阻塞 I/O 的情况下,没有数据可以读,需要继续调用 read。
- 14-15 行表示读到对方发出的 FIN 包,表现形式是 EOF,此时需要关闭套接字。
- 17-18 行,需要读取的字符数减少,缓存指针往下移动。
- 20 行是在读取 EOF 跳出循环后,返回实际读取的字符数。
缓冲区实验
用一个客户端 - 服务器的例子来解释一下读取缓冲区和发送缓冲区的概念。在这个例子中客户端不断地发送数据,服务器端每读取一段数据之后进行休眠,以模拟实际业务处理所需要的时间。
服务器端读取数据程序
#include "lib/common.h"void read_data(int sockfd) {ssize_t n;char buf[1024];int time = 0;for (;;) {fprintf(stdout, "block in readn");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 秒,用来模拟服务器端处理时延。
客户端发送数据程序
#include "lib/common.h"#define MESSAGE_SIZE 102400void 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 长度的字符串流发送出去
实验一: 观察客户端数据发送行为
客户端程序发送了一个很大的字节流,程序运行起来之后,我们会看到服务端不断地在屏幕上打印出读取字节流的过程:
而客户端直到最后所有的字节流发送完毕才打印出下面的一句话,说明在此之前 send 函数一直都是阻塞的,也就是说阻塞式套接字最终发送返回的实际写入字节数和请求字节数是相等的。
实验二: 服务端处理变慢
如果我们把服务端的休眠时间稍微调大,把客户端发送的字节数从 10240000 调整为 1024000,再次运行刚才的例子,我们会发现,客户端很快打印出一句话:
但与此同时,服务端读取程序还在屏幕上不断打印读取数据的进度,显示出服务端读取程序还在辛苦地从缓冲区中读取数据。
通过这个例子我想再次强调一下:发送成功仅仅表示的是数据被拷贝到了发送缓冲区中,并不意味着连接对端已经收到所有的数据。至于什么时候发送到对端的接收缓冲区,或者更进一步说,什么时候被对方应用程序缓冲所接收,对我们而言完全都是透明的。
思考题
既然缓冲区如此重要,我们可不可以把缓冲区搞得大大的,这样不就可以提高应用程序的吞吐量了么?
一段数据流从应用程序发送端,一直到应用程序接收端,总共经过了多少次拷贝?
套接字的作用与一般使用_05 | 使用套接字进行读写相关推荐
- python套接字编程_Python套接字编程(1)——socket模块与套接字编程
在Python网络编程系列,我们主要学习以下内容: 5. 常见的Python异步编程框架 6. 协程在Python网络编程中的使用 本文介绍Python下的基本套接字编程,主要基于 socket 模块 ...
- 《UNIX网络编程 卷1:套接字联网API》学习笔记——基本TCP套接字编程
UNIX网络编程--基本TCP套接字编程 socket 函数 connect 函数 bind 函数 listen 函数 accept 函数 fork 和 exec 函数 并发服务器 close 函数 ...
- 金蝶KIS,从旧账套拷贝供应链单据的自定义字段到全新帐套
K3系列请直接忽略,有官方的直接拷贝工具,详情见k3help.kingdee.com 一般来说自定义字段过多又需要每年重新开账的,建议将恢复一个旧账套然后将数据清除重新录单就行了,或者备份一个初始帐套 ...
- 用计算机来谈生僻字,语义搜索及框计算:从百度查生僻字谈起
三个雷怎么读?男女男念什么?开火是什么字?--相信有不少人都曾经被这样的问题所困惑过.要知道,中国的汉字常用的有4千左右,总数却超过8万,生僻字远远超过常用字,遇到一些不会读.不会输入的生僻字不足为奇 ...
- 套料排版代码python_钣金制造管理系统Fabcost自动套料排版设置
在钣金制造管理系统中,我们套料人员在导入了零件加工清单,材料工程师导入了材料清单之后,就可以开始每天的套料任务.但是这里就有一个小问题,套料人员每天都需要创建很多任务,用于生产部门使用,所以效率是第一 ...
- 计算机一级考试理论题第5套,计算机等级考试一级windows笔试模拟试题5套
计算机等级考试一级windows笔试模拟试题5套 计算机等级考试一级windows笔试模拟试题5套 一.选择题 1.在计算机领域中通常用MIPS来描述______. A.计算机的运算速度 B.计算机的 ...
- 账套输出时文件服务器错误,你为什么会创建账套失败?
原标题:你为什么会创建账套失败? 每当要使用一套新账的时候,第一步免不了就需要先创立新账,然而在创建账套这个步骤上也是遭受了不少的磨难,让小编和您一起将创建账套这件事变得轻松易解决. 一.创建账套失败 ...
- a6账套管理显示无法连接服务器,A6企业管理软件账套管理及初始化流程.ppt
文档介绍: 财务软件部注理岸撰堪腥愉褒缉撅毅宋繁烹可痹庚虞膛窜倍丝轰糊胸齿墓羌谨深顺往A6企业管理软件账套管理及初始化流程A6企业管理软件账套管理及初始化流程二A6企业管理软件操作流程学****倒扶心 ...
- 【T+】将查询账套更换成可以业务操作的正式版账套
[问题描述] 在使用畅捷通T+软件过程中, 由于某些原因,将已使用的账套设置成[查询版],并且根据软件提示,设置后该账套只能查询使用,不可以进行业务操作. 并且[查询]账套不支持反操作. 设置成查询版 ...
- 函授计算机大专自我鉴定100字,大专函授毕业自我鉴定300字大专通用,大专函授毕业自我鉴定100字...
大专函授毕业自我鉴定300字大专通用,大专函授毕业自我鉴定100字 时间:2018-09-01 00:23:34 作者:知网查重入口 阅读:次 这些企业清算方面的法律规定的问题处理使企业清算有法可依, ...
最新文章
- EditText的另类用法
- Kafka集群安装--测试--关闭
- 训练日志 2019.3.10
- js浏览器页面生命周期
- 完整的 .NET Core 目标框架的预处理器符号列表
- Spring 解耦工厂模式
- 集结500+CEO北京共“吹牛”,移动认证又搞什么大事情?
- asp.net生产环境和开发环境的错误日志包装策略
- 图像处理之形态学梯度计算
- 在Linux下如何使用GCC编译程序、简单生成静态库及动态库。
- fiddler2抓包工具使用图文教程
- 微博热搜榜html源码,几行代码完成微博热搜榜爬虫
- 翻车!误删/usr/lib/引发的血案,从棺材边成功抢救的过程分享。
- ArcGIS Pro中管线三维符号化方法
- 如何使用物联网低代码平台进行设备调试?
- FOB指定货操作标准流程及相关经验
- Linux系统 运行小花仙游戏(针对2021年Flash停止维护的情况)
- Python爬虫学习手册
- 当网红本人成为网红毒瘤:Vtuber的纸片人模式能够破解困局吗?
- python画k线图_python自动获取行情数据,并画k线图
热门文章
- 拓端tecdat|Python支持向量回归SVR拟合、预测回归数据和可视化准确性检查实例
- 拓端tecdat|R语言用ARIMA模型,ARIMAX模型预测冰淇淋消费时间序列数据
- java xml编译_IDEA将Maven项目中指定文件夹下的xml等文件编译进classes的方法
- cannot import name ‘Imputer‘ from ‘sklearn.preprocessing‘
- 计算机存储地址如何,计算机内存地址只有5种表现形式吗,为什么?
- Java泛型报错的解决办法
- 操作系统 第二部分 进程管理(五)
- FreeRTOS基础认识
- Python-OpenCV人脸检测(代码)
- 服务器真在运行中,win7电脑服务器正在运行中的解决教程