网络编程网络编程相关知识点总结1
Posted trevo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程网络编程相关知识点总结1相关的知识,希望对你有一定的参考价值。
bind
客户端可以调用bind函数吗?可以,可以指定端口 详见复习资料
客户端为何不调用bind函数,什么时候像套接字分配IP和端口号
listen
它现在定义的是已完成连接队列的最大长度,表示的是已建立的连接(established connection),正在等待被接收(accept 调用返回),而不是原先的未完成队列的最大长度
connect
TCP套接字调用其出错返回的可能情况如下:
- 三次握手无法建立,客户端发出的SYN包没有任何响应,返回TIMEOUT错误,这种情况比较常见的原因是对应的服务端IP写错;
- 客户端收到RST回答,这时候客户端会返回CONNECTION REFUSED错误。这种情况常见于客户端发送连接请求时端口写错;
- 客户发出的SYN包在网络上引起了destination unreachable即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。
三次握手
准备工作
-
服务器端经过socket,bind和listen完成被动套接字的准备工作,然后调用accept阻塞等待客户端的连接;
-
客户端通过调用socket,connect,也会阻塞;
-
由操作系统(内核网络协议栈)来处理。
三次握手过程
-
(第一次握手)客户端协议栈向服务器端发送SYN包,并告诉服务器当前发送的序列号j,客户端进入SYN_SENT状态;
-
(第二次握手)服务器端协议栈收到这个包之后,与客户端进行ACK应答,应答的值为j+1,表示对SYN包j的确认,同时服务器也发送一个SYN包,告诉客户端我当前发送的序列号是k,服务器端进入SYN_RCVD状态;
-
(第三次握手)客户端协议栈收到ACK之后,使得应用程序从connect调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为ESTABLISHED,同时客户端协议栈也会对服务器端的SYN包进行应答,应答数据为k+1; 应答包到达服务器后,服务器端协议栈使得accept阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入ESTABLISHED状态。
面试题:
1. 客户端的第三次应答,服务器没有收到会怎样?
第三次的ACK在网络中丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。而Server重发SYN+ACK包的次数,可以通过设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.
如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。
client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。
2. TCP连接的建立为什么是三次?
讲道理,C要确认S是否能够正常收发消息,需要发一条消息给S,并且接受S的一条确认消息才行,这一来一回就是两条。同理,S要确认与C的连接,也需要这样。总共就是四次通信。三次握手把中间两次给合并为了一次,减少资源的消耗;
一次握手、两次握手都确认不了C和S的收发消息的能力是否OK,三次握手是比较简洁有效的方式,大于三次以上的握手机制也可以确认,不过有些浪费资源,毕竟三次就能搞定的事情,没必要搞三次以上。
阻塞调用 vs 非阻塞调用
使用场景
区别
缓冲区
数据发送过程中,往往是将借助io函数数据从应用程序中拷贝到操作系统内核的(接收)发送缓冲区中;一下两种情况。
- 操作系统内核的发送缓冲区足够大?io函数直接返回
- 发送缓冲区很大,数据没有发完
- 数据发完了,操作系统内核的发送缓冲区不足以容纳数据
2和3两种情况发生时,操作系统内核不会立即返回,也不报错,而是程序别阻塞。等到应用程序数据完全放到操作系统内核的发送缓冲区中,再从系统调用中返回。
理解缓冲区
/*服务器端代码*/
#include <strings.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.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)表示对端发送FIN包,套接字关闭 */
length -= result;
buffer_pointer += result;
}
return (size - length); /* 返回的是实际读取的字节数*/
}
/*每次从缓冲区中读取1024个字节*/
void read_data(int sockfd) {
ssize_t n;
char buf[1024];
int time = 0;
for (;;) {
fprintf(stdout, "block in read ");
if ((n = readn(sockfd, buf, 1024)) == 0)
return;
time++;
fprintf(stdout, "1K read for %d ", time);
fprintf(stdout, "
");
usleep(100000);
}
}
int main(int argc, char **argv) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
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 (;;) {
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
read_data(connfd); /* 读取数据 */
close(connfd); /* 关闭连接套接字,注意不是监听套接字*/
}
close(listenfd);
return 0;
}
/*客户端代码*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define MESSAGE_SIZE 10240000
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] = ‘ ‘;
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 %d
", 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);
}
发送成功仅仅表示数据被拷贝到了发送缓冲区中,并不意味着链接对端已经收到所有的数据,至于什么时候发送到对端的接受缓冲区,或者什么时候被对方应用程序缓冲所接受,对我们来说完全是透明的。
面试题
1. 一段数据流从应用程序发送端一直到应用程序接受端,总共拷贝了几次?
发送端,当应用程序将数据发送到发送缓冲区时,调用的是 send 或 write 方法,如果缓存中没有空间,系统调用就会失败或者阻塞。我们说,这个动作事实上是一次“显式拷贝”。而在这之后,数据将会按照 TCP/IP 的分层再次进行拷贝,这层的拷贝对我们来说就不是显式的了。接下来轮到 TCP 协议栈工作,创建 Packet 报文,并把报文发送到传输队列中(qdisc),传输队列是一个典型的 FIFO 队列,队列的最大值可以通过 ifconfig 命令输出的 txqueuelen 来查看。通常情况下,这个值有几千报文大小。TX ring 在网络驱动和网卡之间,也是一个传输请求的队列。网卡作为物理设备工作在物理层,主要工作是把要发送的报文保存到内部的缓存中,并发送出去。
接收端,报文首先到达网卡,由网卡保存在自己的接收缓存中,接下来报文被发送至网络驱动和网卡之间的 RX ring,网络驱动从 RX ring 获取报文 ,然后把报文发送到上层。这里值得注意的是,网络驱动和上层之间没有缓存,因为网络驱动使用 Napi 进行数据传输。因此,可以认为上层直接从 RX ring 中读取报文。最后,报文的数据保存在套接字接收缓存中,应用程序从套接字接收缓存中读取数据
四次挥手
- 第一次挥手:主机1先发送FIN m报文,主机2进入CLOSE_WAIT状态;
- 第二次挥手:主机2发送一个ACK m+1应答;
- 第三次挥手:主机2通过read调用得到EOF,将结果通知应用程序主动关闭操作,发送FIN n报文。主机接收到FIN n报文;
- 第四次挥手:主机1在接收到FIN n报文后发送ACK n+1应答,进入TIME_WAIT状态。主机2收到ACK后,进入CLOSED状态,主机1在TIME_WAIT停留 持续时间是固定的,是2*MSL。
TIME_WAIT
只有发起连接终止的一方才会进入TIME_WAIT状态。
作用
- 确保最后的ACK能够让被动关闭放接收,从而帮助其正常关闭。
- 与连接“化身”和报文迷走有关,为了让旧连接的重复分节在网络中自然消失。
危害
- 内存资源占用,不是很严重,基本可以忽略。
- 端口资源的占用,一个TCP连接至少消耗一个本地端口,端口资源也是有限的,一般可以开启的端口为32768~61000。通过设置net.ipv4.ip_local_port_range指定。如果TIME_WAIT状态过多,会导致无法创建新连接。
优化
- 通过sysctl命令,将net.ipv4_tcp_max_tw_buckets这个值调小,默认为18000,当系统处于TIME_WAIT的连接一旦超出这个值,系统就会将所有TIME_WAIT连接状态重置,并且只打印出警告信息;
- 重新编译系统,调低TCP_TIMEWAIT_LEN;
- 通过设置套接字选项SO_LINGER,调整close或者shutdown关闭连接时的行为。
- 更安全的做法:设置net.ipv4.tcp_tw_reuse
shutdown优雅关闭
TCP是双向的,双向指的是数据流的写入-读出方向
close如何关闭连接?
close关闭连接是对套接字引用计数减一,一旦返现引用计数为0,彻底释放套接字,关闭TCP两个方向的数据流。
具体些,如何关闭两个方向的数据流?
-
输入方向,系统内核将该套接字设置为不可读,任何读操作都异常
-
输出方向,系统内核尝试将发送缓冲区的数据发送到对端,并最后相对端发送一个FIN报文,接下来任何对该套接字的写操作都会异常。
如果对端还是没有检测到套接字已关闭,继续发送报文,接收端收到一个RST报文,告诉对端,我已关闭,不要给我发数据了。
shutdown
可以关闭连接的一个方向
SHUT_RD-读 、SHUT_WR-写、SHUT_RDWR-读+写
shutdown与close的区别?
close | shutdown | |
---|---|---|
资源释放 | 关闭后,会释放所有连接对应的资源 | 关闭后,不会释放掉套接字和所有资源 |
引用计数 | close存在引用计数,不一定会导致该套接字不可用 | 不存在,直接使得该套接字不可用 |
FIN结束报文 | close有引用计数,不一定会发出FIN | 总是发出FIN结束报文 |
服务器端
接收客户端的应答显示到标准输出上
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define MAXLINE 4096
#define SERV_PORT 9091
static int count;
/*信号处理函数,避免程序莫名退出*/
static void sig_int(int signo) {
printf("
received %d datagrams
", count);
exit(0);
}
int main(int argc, char **argv) {
/*创建套接字*/
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
/*绑定端口号和ip*/
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
}
/*转为被动套接字*/
int rt2 = listen(listenfd, 5);
if (rt2 < 0) {
error(1, errno, "listen failed ");
}
/*信号处理,避免程序莫名退出*/
signal(SIGINT, sig_int);
signal(SIGPIPE, SIG_DFL);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
}
char message[MAXLINE];
count = 0;
for (;;) {
/*获取客户端数据*/
int n = read(connfd, message, MAXLINE);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed
");
}
/*格式化原字符*/
message[n] = 0;
printf("received %d bytes: %s
", n, message);
count++;
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
sleep(5);
/*发送数据给客户端*/
size_t write_nc = send(connfd, send_line, strlen(send_line), 0);
printf("send bytes: %zu
", write_nc);
if (write_nc < 0) {
error(1, errno, "error write");
}
}
}
客户端
从标准输入不断接受用户输入,把输入的字符串通过套接字发送给服务器端。
- 输入close,调用close关闭连接,休眠一段时间,等待服务器端处理后退出。
- 输入shutdown函数关闭连接的写方向,不会直接退出,等待服务器端的应答,直到服务器端完成自己的操作,在另一个方向上完成关闭。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define MAXLINE 4096
#define SERV_PORT 9091
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: graceclient <IPaddress>");
}
/*创建套接字*/
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
/*设置端口号和ip*/
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
}
char send_line[MAXLINE], recv_line[MAXLINE + 1];
int n;
fd_set readmask;
fd_set allreads;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, NULL);
if (rc <= 0)
error(1, errno, "select failed");
/*有数据可读,将数据读入程序缓冲区*/
if (FD_ISSET(socket_fd, &readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read error");
} else if (n == 0) {
error(1, 0, "server terminated
");
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("
", stdout);
}
/*当标准输入上有数据可读,读入后判断*/
if (FD_ISSET(0, &readmask)) {
if (fgets(send_line, MAXLINE, stdin) != NULL) {
/*shutdown关闭标注输入的I/O事件感知*/
if (strncmp(send_line, "shutdown", 8) == 0) {
FD_CLR(0, &allreads);
if (shutdown(socket_fd, 1)) {
error(1, errno, "shutdown failed");
}
/*close关闭连接*/
} else if (strncmp(send_line, "close", 5) == 0) {
FD_CLR(0, &allreads);
if (close(socket_fd)) {
error(1, errno, "close failed");
}
sleep(6);
exit(0);
/*处理正常输入,把回车符截掉*/
} else {
int i = strlen(send_line);
if (send_line[i - 1] == ‘
‘) {
send_line[i - 1] = 0;
}
printf("now sending %s
", send_line);
size_t rt = write(socket_fd, send_line, strlen(send_line));
if (rt < 0) {
error(1, errno, "write failed ");
}
printf("send bytes: %zu
", rt);
}
}
}
}
}
思考题:
-
服务器端程序中为什么调用exit(0)完成了FIN报文发送?为啥不调用close或者shutdown
因为在调用exit之后进程会退出,而进程相关的所有的资源,文件,内存,信号等内核分配的资源都会被释放,在linux中,一切皆文件,本身socket就是一种文件类型,内核会为每一个打开的文件创建file结构并维护指向改结构的引用计数,每一个进程结构中都会维护本进程打开的文件数组,数组下标就是fd,内容就指向上面的file结构,close本身就可以用来操作所有的文件,做的事就是,删除本进程打开的文件数组中指定的fd项,并把指向的file结构中的引用计数减一,等引用计数为0的时候,就会调用内部包含的文件操作close,针对于socket,它内部的实现应该就是调用shutdown,只是参数是关闭读写端,从而比较粗暴的关闭连接。
-
信号量处理中,默认处理和自定义函数的区别?
信号的处理有三种,默认处理,忽略处理,自定义处理。默认处理就是采用系统自定义的操作,大部分信号的默认处理都是杀死进程,忽略处理就是当做什么都没有发生。
连接状态的检测
keep-alive
TCP保持活跃机制,定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到相应,则认为当前的TCP连接已经死亡,系统内核将错误信息通知给上层应用程序。
相关的可定义变量systctl变量(linux)和默认值
- 保活时间:net.ipv4.tcp_keepalive_time = 7200s
- 保活时间间隔:net.ipv4.tcp_keepalive_intvl = 75s
- 保活探测次数:net.ipv4.tcp_keepalive_probes = 9次
开启TCP保活,有以下几种情况
-
对端程序是正常工作的
当TCP保活的探测报文发送给对端,对端会正常响应,这样TCP保活时间会被重置。
-
对端程序崩溃并重启
当TCP保活的探测报文发送给对端后,对端是可以相应的,但是由于没有该连接的有效信息,会产生一个RST报文,这样很快就会发现TCP链接已经被重置。
-
是对端程序崩溃,或对端由于其他原因导致报文不可达
当TCP保活的探测报文发送给对端后,石沈大海,没有响应连续几次,达到保活探测次数后,TCP会报告该TCP连接已经死亡。
TCP保活机制默认是关闭的,可以选择在连接的两个方向开启,也可以单独在一个方向上开启。如果开启服务器端到客户端的检测,可以再客户端非正常断连的情况下清楚服务器端保留的脏数据;而开启客户端到服务器端的检测,可以在服务器无响应的情况下,重新发起连接。
面试题:
-
为什么TCP不提供频率较高的保活机制呢?
早期的网络宽带非常有限,如果提供一个频率很高的保活机制,对有限的带宽是一个比较严重的浪费。
应用层探活
使用TCP自身的保活机制,时间间隔比较长,对于有时延要求的系统是无法接受的。所以必须在应用层寻找好的解决方案。
可以设计一个PING-PONG的机制,需要保活的一方,比如客户端,在保活时间到达后,发起对连接的PING操作,如果服务器端对PING有回应,则重新设置保活时间,否则对探测次数进行计数,如果最终探测次数达到了保活探测次数预先设置的值之后,则认为连接无效。
关键点:
- 需要使用定时器,通过使用I/O复用自身的机制来实现;
- 需要设计一个PING-PONG的协议。
消息格式设计
四种类型消息
#ifndef MESSAGE_OBJECTE_H
#define MESSAGE_OBJECTE_H
#include <sys/types.h>
typedef struct {
u_int32_t type;
char data[1024];
} messageObject;
#define MSG_PING 1
#define MSG_PONG 2
#define MSG_TYPE1 11
#define MSG_TYPE2 21
#endif //MESSAGE_OBJECTE_H
客户端
模拟TCP Keep-Alive的机制,在保活时间达到后,探活次数加一,同时向服务器发送PING格式的消息。此后以预设的保活时间间隔,不断向服务器发送PING格式的消息,如果收到服务器端的应答,则结束报货,将保活时间置0;
使用select自带的定时器
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#include "message_objecte.h"
#define MAXLINE 4096
#define KEEP_ALIVE_TIME 10
#define KEEP_ALIVE_INTERVAL 3
#define KEEP_ALIVE_PROBETIMES 3
#define SERV_PORT 9091
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: tcpclient <IPaddress>");
}
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
}
char recv_line[MAXLINE + 1];
int n;
fd_set readmask;
fd_set allreads;
struct timeval tv;
int heartbeats = 0;
/*设置超时时间,即保活时间*/
tv.tv_sec = KEEP_ALIVE_TIME;
tv.tv_usec = 0;
messageObject messageObject;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(socket_fd, &allreads);
for (;;) {
readmask = allreads;
int rc = select(socket_fd + 1, &readmask, NULL, NULL, &tv);
if (rc < 0) {
error(1, errno, "select failed");
}
/*
客户端已经在KEEP_ALIVE_TIME这段时间内没有收到任何当前连接的反馈,于是发起PING消息
通过传送一个类型为MSG_PING的消息对象来完成PING操作
*/
if (rc == 0) {
if (++heartbeats > KEEP_ALIVE_PROBETIMES) {
error(1, 0, "connection dead
");
}
printf("sending heartbeat #%d
", heartbeats);
messageObject.type = htonl(MSG_PING);
rc = send(socket_fd, (char *) &messageObject, sizeof(messageObject), 0);
if (rc < 0) {
error(1, errno, "send failure");
}
tv.tv_sec = KEEP_ALIVE_INTERVAL;
continue;
}
/*
在接收到服务器端程序之后的处理
实际中应该对报文进行解析后处理,只要PONG类型的回应才是PING探活的结果。
这里认为只要收到服务器端的报文,连接就是正常,探活计数器和探活时间都置零,等待下一次探活时间的来临
*/
if (FD_ISSET(socket_fd, &readmask)) {
n = read(socket_fd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read error");
} else if (n == 0) {
error(1, 0, "server terminated
");
}
printf("received heartbeat, make heartbeats to 0
");
heartbeats = 0;
tv.tv_sec = KEEP_ALIVE_TIME;
}
}
}
服务器端
服务器端的程序在收到客户端发来的各种消息后,进行处理,发现如果是PING类型的消息休眠一段时间后回复一个PONG消息;如果休眠时间很长,客户端就无法知道服务器端是否存活,实际情况应该是系统崩溃或者网络异常。
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#include "message_objecte.h"
#define SERV_PORT 9091
static int count;
static void sig_int(int signo) {
printf("
received %d datagrams
", count);
exit(0);
}
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: tcpsever <sleepingtime>");
}
int sleepingTime = atoi(argv[1]);
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERV_PORT);
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
}
int rt2 = listen(listenfd, 5);
if (rt2 < 0) {
error(1, errno, "listen failed ");
}
signal(SIGINT, sig_int);
signal(SIGPIPE, SIG_IGN);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
}
messageObject message;
count = 0;
for (;;) {
int n = read(connfd, (char *) &message, sizeof(messageObject));
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed
");
}
printf("received %d bytes
", n);
count++;
switch (ntohl(message.type)) {
/*处理MSG_TYPE1的消息*/
case MSG_TYPE1 :
printf("process MSG_TYPE1
");
break;
/*处理MSG_TYPE2的消息*/
case MSG_TYPE2 :
printf("process MSG_TYPE2
");
break;
/*
处理MSG_PING的消息
通过休眠模拟相应是否及时,调用send发送一个PONG报文
*/
case MSG_PING: {
messageObject pong_message;
pong_message.type = MSG_PONG;
sleep(sleepingTime);
ssize_t rc = send(connfd, (char *) &pong_message, sizeof(pong_message), 0);
if (rc < 0)
error(1, errno, "send failure");
break;
}
/*异常行为处理,消息格式不认识,程序出错退出*/
default :
error(1, 0, "unknown message type (%d)
", ntohl(message.type));
}
}
}
这种保活机制的建立依赖于系统定时器和适合的应用层报文协议。
面试题:
-
TCP探活的方法适用于UDP吗?
? UDP里面各方并不会维护一个socket上下文状态是无连接的,如果为了连接而保活是不必要的,如果为了探测对端是否正常工作而做ping-pong也是可行的。
-
额外的探活报文占用了有限带宽?为什么需要多次探活才能决定一个TCP连接是否已死。
? 额外的探活报文是会占用一些带宽资源,可根据实际业务场景,适当增加保活时间,降低探活频率,简化ping-pong协议。有必要判断存活,举一个打游戏的例子,电脑突然蓝屏,但是游戏的角色还残留在游戏中,所以服务器为了判断它是否真的存活还是需要一个心跳包,隔一段时间过后把它踢下线。
? 多次探活是为了防止误伤,避免ping包在网络中丢失掉了,而误认为对端死亡。
TCP协议中的动态数据传输
禁用Nagle算法
SO_REUSEADDR
TIME_WAIT会导致Address already in use的错误
Linux对端口重用问题的优化
- 新连接SYN告知的初始序列号,一定比TIME_WAIT老连接的末序列号大,这样通过序列号就可以区别出新老连接;
- 开启了tcp_timestamps,使得新连接的时间戳比老连接的时间戳大,通过时间戳也可以区别出新旧连接;
由于1和2,一个TIME_WAIT的TC连接链接可以忽略掉旧连接,重新被新的连接接使用,通过设置套接字选项SO_REUSEADDR来实现。该选项允许启动绑定在一个端口上,即使之前存在一个和该端口一样的链接。
int flag = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
总结:服务器端程序,在bind之前,都应该设置SO_REUSEADDR套接字选项,以便服务器端程序可以再较短的时间复用一个端口启动。
面试题
-
TCP可以使用SO_REUSEADDR,UDP可以使用吗
UDP的SO_REUSEADDR使用场景比较多的是组播网络,好处是,如我们在接收组播流的时候,比如用ffmpeg拉取了一个组播流,但是还想用ffmpeg拉取相同的组播流,这个时候就需要地址重用了
-
服务器端程序中,为什么设置SO_REUSEADDR要在bind函数之前对监听的套接字进行设置,而不是对已连接的套接字进行设置。
因为SO_REUSEADDR是针对新建立的连接才起作用,对已建立的连接设置是无效的
流
滑动窗口
对方主机的输入缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?
TCP 中有滑动窗口控制协议,所以传输的时候会保证传输的字节数小于等于自己能接受的字节数。
网络字节序
- 大端字节序:将高字节存放在起始地址(低地址),为网络字节序
- 小端字节序:将低字节存放在起始地址(低地址)
网络字节序转换相关函数
n表示network,h表示host,s表示short,l表示long,分别表示16位和32位整数
报文读取和解析
如何确定报文的边界
-
发送端要把发送的报文长度预先通过报文告知给接收端
报文格式:
?
发送报文
#include <string.h> #include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/socket.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <error.h> #define SERV_PORT 9091 int main(int argc, char **argv) { if (argc != 2) { error(1, 0, "usage: tcpclient <IPaddress>"); } int socket_fd; socket_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &server_addr.sin_addr); socklen_t server_len = sizeof(server_addr); int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len); if (connect_rt < 0) { error(1, errno, "connect failed "); } struct { u_int32_t message_length; u_int32_t message_type; char data[128]; } message; int n; while (fgets(message.data, sizeof(message.data), stdin) != NULL) { n = strlen(message.data); message.message_length = htonl(n); message.message_type = htonl(1); if (send(socket_fd, (char *) &message, sizeof(message.message_length) + sizeof(message.message_type) + n, 0) < 0) error(1, errno, "send failure"); } exit(0); }
解析报文
#include <string.h> #include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/socket.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <error.h> #define SERV_PORT 9091 static int count; static void sig_int(int signo) { printf(" received %d datagrams ", count); exit(0); } 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); /* 返回的是实际读取的字节数*/ } size_t read_message(int fd, char *buffer, size_t length) { u_int32_t msg_length; u_int32_t msg_type; int rc; /* Retrieve the length of the record */ rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t)); if (rc != sizeof(u_int32_t)) return rc < 0 ? -1 : 0; msg_length = ntohl(msg_length); rc = readn(fd, (char *) &msg_type, sizeof(msg_type)); if (rc != sizeof(u_int32_t)) return rc < 0 ? -1 : 0; /* 判断buffer是否可以容纳下数据 */ if (msg_length > length) { return -1; } /* Retrieve the record itself */ rc = readn(fd, buffer, msg_length); if (rc != msg_length) return rc < 0 ? -1 : 0; return rc; } int main(int argc, char **argv) { int listenfd; listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(SERV_PORT); int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr)); if (rt1 < 0) { error(1, errno, "bind failed "); } int rt2 = listen(listenfd, 5); if (rt2 < 0) { error(1, errno, "listen failed "); } signal(SIGPIPE, SIG_IGN); int connfd; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) { error(1, errno, "bind failed "); } char buf[128]; count = 0; while (1) { int n = read_message(connfd, buf, sizeof(buf)); if (n < 0) { error(1, errno, "error read message"); } else if (n == 0) { error(1, 0, "client closed "); } buf[n] = 0; printf("received %d bytes: %s ", n, buf); count++; } exit(0); }
-
通过一些特殊的字符来进行边界的划分
HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界
int read_line(int fd, char *buf, int size) { int i = 0; char c = ‘ ‘; int n; while ((i < size - 1) && (c != ‘ ‘)) { n = recv(fd, &c, 1, 0); if (n > 0) { if (c == ‘ ‘) { n = recv(fd, &c, 1, MSG_PEEK); if ((n > 0) && (c == ‘ ‘)) recv(fd, &c, 1, 0); else c = ‘ ‘; } buf[i] = c; i++; } else c = ‘ ‘; } buf[i] = ‘ ‘; return (i); }
TCP的可靠性
超时重传
TCP通过超时重传机制来保证丢失数据的可靠传输,如果报文发出去的特定时间内,发送消息的主机没有收到另一个主机的回复,那么就继续发送这条消息,直到收到回复为止。
TCP是否可靠?
TCP 是一种可靠的协议,这种可靠体现在端到端的通信上。这似乎给我们带来了一种错觉,从发送端来看,应用程序通过调用 send 函数发送的数据流总能可靠地到达接收端;而从接收端来看,总是可以把对端发送的数据流完整无损地传递给应用程序来处理。这种理解是不对的!!!
发送端通过调用 send 函数之后,数据流并没有马上通过网络传输出去,而是存储在套接字的发送缓冲区中,由网络协议栈决定何时发送、如何发送。当对应的数据发送给接收端,接收端回应 ACK,存储在发送缓冲区的这部分数据就可以删除了,但是,发送端并无法获取对应数据流的 ACK 情况,也就是说,发送端没有办法判断对端的接收方是否已经接收发送的数据流,如果需要知道这部分信息,就必须在应用层自己添加处理逻辑,例如显式的报文确认机制。从接收端来说,也没有办法保证 ACK 过的数据部分可以被应用程序处理,因为数据需要接收端程序从接收缓冲区中拷贝,可能出现的状况是,已经 ACK 的数据保存在接收端缓冲区中,接收端处理程序突然崩溃了,这部分数据就没有办法被应用程序继续处理。
TCP 连接建立之后,能感知 TCP 链路的方式是有限的,一种是以 read 为核心的读操作,另一种是以 write 为核心的写操作。
故障模式演示
对端无FIN包发送
1. 网络中断导致的
很多原因都会造成网络中断,在这种情况下,TCP 程序并不能及时感知到异常信息。除非网络中的其他设备,如路由器发出一条 ICMP 报文,说明目的网络或主机不可达,这个时候通过 read 或 write 调用就会返回 Unreachable 的错误。
可惜大多数时候并不是如此,在没有 ICMP 报文的情况下,TCP 程序并不能理解感应到连接异常。如果程序是阻塞在 read 调用上,那么很不幸,程序无法从异常中恢复。这显然是非常不合理的,不过,我们可以通过给 read 操作设置超时来解决。在接下来的第 18 讲中,我会讲到具体的方法。如果程序先调用了 write 操作发送了一段数据流,接下来阻塞在 read 调用上,结果会非常不同。
Linux 系统的 TCP 协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传 12 次、合计时间约为 9 分钟之后,协议栈会标识该连接异常,这时,阻塞的 read 调用会返回一条 TIMEOUT 的错误信息。如果此时程序还执着地往这条连接写数据,写操作会立即失败,返回一个 SIGPIPE 信号给应用程序
2. 系统崩溃导致的
当系统突然崩溃,如断电时,网络连接上来不及发出任何东西。这里和通过系统调用杀死应用程序非常不同的是,没有任何 FIN 包被发送出来。这种情况和网络中断造成的结果非常类似,在没有 ICMP 报文的情况下,TCP 程序只能通过 read 和 write 调用得到网络连接异常的信息,超时错误是一个常见的结果。
不过还有一种情况需要考虑,那就是系统在崩溃之后又重启,当重传的 TCP 分组到达重启后的系统,由于系统中没有该 TCP 分组对应的连接数据,系统会返回一个 RST 重置分节,TCP 程序通过 read 或 write 调用可以分别对 RST 进行错误处理。
- 如果是阻塞的 read 调用,会立即返回一个错误,错误信息为连接重置(Connection Reset)。如果是一次 write 操作,也会立即失败,应用程序会被返回一个 SIGPIPE 信号。
对端有FIN包的演示
服务器端程序
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091
int tcp_server(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
}
int rt2 = listen(listenfd, 5);
if (rt2 < 0) {
error(1, errno, "listen failed ");
}
signal(SIGPIPE, SIG_IGN);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
}
return connfd;
}
int main(int argc, char **argv) {
int connfd;
char buf[1024];
connfd = tcp_server(SERV_PORT);
for (;;) {
int n = read(connfd, buf, 1024);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed
");
}
sleep(5);
int write_nc = send(connfd, buf, n, 0);
printf("send bytes: %d
", write_nc);
if (write_nc < 0) {
error(1, errno, "error write");
}
}
exit(0);
}
客户端程序
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
# define MESSAGE_SIZE 102400000
#define SERV_PORT 9091
int tcp_client(char *address, int port) {
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, address, &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
}
return socket_fd;
}
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: reliable_client01 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
char buf[129];
int len;
int rc;
while (fgets(buf, sizeof(buf), stdin) != NULL) {
len = strlen(buf);
rc = send(socket_fd, buf, len, 0);
if (rc < 0)
error(1, errno, "write failed");
sleep(3);
rc = read(socket_fd, buf, sizeof(buf));
if (rc < 0)
error(1, errno, "read failed");
else if (rc == 0)
error(1, 0, "peer connection closed
");
else
fputs(buf, stdout);
}
exit(0);
}
1. read直接感知FIN包
依次启动服务器端和客户端,在服务器端输入good字符之后,迅速结束服务器端
2. 通过write产生RST,read调用感知RST
依次启动服务器端和客户端,在服务器端输入bad字符之后,等待服务器端收到,再次杀死服务器端,客户端再次输入bad2
服务器端程序
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091
int tcp_server(int port) {
int listenfd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (rt1 < 0) {
error(1, errno, "bind failed ");
}
int rt2 = listen(listenfd, 5);
if (rt2 < 0) {
error(1, errno, "listen failed ");
}
signal(SIGPIPE, SIG_IGN);
int connfd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if ((connfd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len)) < 0) {
error(1, errno, "bind failed ");
}
return connfd;
}
int main(int argc, char **argv) {
int connfd;
char buf[1024];
int time = 0;
connfd = tcp_server(SERV_PORT);
while (1) {
int n = read(connfd, buf, 1024);
if (n < 0) {
error(1, errno, "error read");
} else if (n == 0) {
error(1, 0, "client closed
");
}
time++;
fprintf(stdout, "1K read for %d
", time);
usleep(10000);
}
exit(0);
}
客户端程序
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <error.h>
#define SERV_PORT 9091
# define MESSAGE_SIZE 102400
int tcp_client(char *address, int port) {
int socket_fd;
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, address, &server_addr.sin_addr);
socklen_t server_len = sizeof(server_addr);
int connect_rt = connect(socket_fd, (struct sockaddr *) &server_addr, server_len);
if (connect_rt < 0) {
error(1, errno, "connect failed ");
}
return socket_fd;
}
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: reliable_client02 <IPaddress>");
}
int socket_fd = tcp_client(argv[1], SERV_PORT);
signal(SIGPIPE, SIG_IGN);
char *msg = (char *)"network programming";
ssize_t n_written;
int count = 10000000;
while (count > 0) {
n_written = send(socket_fd, msg, strlen(msg), 0);
fprintf(stdout, "send into buffer %ld
", n_written);
if (n_written <= 0) {
error(1, errno, "send error");
return -1;
}
count--;
}
return 0;
}
3. 向一个已关闭连接连续写,最终导致SIGPIPE
如果在服务端读取数据并处理过程中,突然杀死服务器进程,我们会看到客户端很快也会退出
检查数据的有效性
缓冲区处理的错误演示
缓冲区溢出
缓冲区溢出,是指计算机程序中出现的一种内存违规操作。本质是计算机程序向缓冲区填充的数据,超出了原本缓冲区设置的大小限制,导致了数据覆盖了内存栈空间的其他合法数据。这种覆盖破坏了原来程序的完整性,导致应用程序崩溃。
char Response[] = "COMMAND OK";
char buffer[128];
while (1) {
int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
if (nBytes == -1) {
error(1, errno, "error read message");
} else if (nBytes == 0) {
error(1, 0, "client closed
");
}
//可能发生buffer[128] = ‘ ‘的情况
buffer[nBytes] = ‘ ‘;
if (strcmp(buffer, "quit") == 0) {
printf("client quit
");
send(socket, Response, sizeof(Response), 0);
}
printf("received %d bytes: %s
", nBytes, buffer);
}
解决办法:留下 buffer 里的一个字节,以容纳后面的‘ ‘
int nBytes = recv(connfd, buffer, sizeof(buffer) - 1, 0);
你会发现我们发送过去的字符串,调用的是sizeof,那也就意味着,Response 字符串中的‘ ‘是被发送出去的,而我们在接收字符时,则假设没有‘ ‘字符的存在。为了统一,我们可以改成如下的方式,使用 strlen 的方式忽略最后一个‘ ‘字符。
send(socket, Response, strlen(Response), 0);
对变长报文解析的两种手段
将报文信息的长度编码进入消息
size_t read_message(int fd, char *buffer, size_t length) {
u_int32_t msg_length;
u_int32_t msg_type;
int rc;
rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
if (rc != sizeof(u_int32_t))
return rc < 0 ? -1 : 0;
msg_length = ntohl(msg_length);
rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
if (rc != sizeof(u_int32_t))
return rc < 0 ? -1 : 0;
/*对实际的报文长度msg_length和应用程序分配的缓冲区大小进行了比较,很重要!!*/
/*如果不判断 msg_length比length大,那么会产生缓冲区溢出*/
if (msg_length > length) {
return -1;
}
/* Retrieve the record itself */
rc = readn(fd, buffer, msg_length);
if (rc != msg_length)
return rc < 0 ? -1 : 0;
return rc;
}
使用特殊的边界符号
使用换行符作为边界符号
-
一个简单的想法是每次读取一个字符,判断这个字符是不是换行符。这里有一个这样的函数,这个函数的最大问题是工作效率太低,要知道每次调用 recv 函数都是一次系统调用,需要从用户空间切换到内核空间,上下文切换的开销对于高性能来说最好是能省则省。
size_t readline(int fd, char *buffer, size_t length) { char *buf_first = buffer; char c; while (length > 0 && recv(fd, &c, 1, 0) == 1) { *buffer++ = c; length--; if (c == ‘ ‘) { *buffer = ‘ ‘; return buffer - buf_first; } } return -1; }
-
一次性读取最多 512 字节到临时缓冲区,之后将临时缓冲区的字符一个一个拷贝到应用程序最终的缓冲区中,这样的做法明显效率会高很多。
size_t readline(int fd, char *buffer, size_t length) { char *buf_first = buffer; static char *buffer_pointer; int nleft = 0; static char read_buffer[512]; char c; while (length-- > 0) { if (nleft <= 0) {//判断临时缓冲区的字符有没有被全部拷贝完 //如果被全部拷贝完,就会再次尝试读取最多 512 字节 int nread = recv(fd, read_buffer, sizeof(read_buffer), 0); if (nread < 0) { if (errno == EINTR) { length++; continue; } return -1; } if (nread == 0) return 0; //在读取字符成功之后,重置了临时缓冲区读指针、临时缓冲区待读的字符个数 buffer_pointer = read_buffer; nleft = nread; } //在拷贝临时缓冲区字符,每次拷贝一个字符,并移动临时缓冲区读指针,对临时缓冲区待读的字符个数进行减 1 操作 c = *buffer_pointer++; *buffer++ = c; nleft--; if (c == ‘ ‘) {//判断是否读到换行符,如果读到则将应用程序最终缓冲区截断,返回最终读取的字符个数 *buffer = ‘ ‘; return buffer - buf_first; } } return -1; }
//输入字符为: 012345678 char buf[10] readline(fd, buf, 10) /*当读到最后一个 字符时,length 为 1,问题是在第 30 行和 31 行,如果读到了换行符,就会增加一个字符串截止符,这显然越过了应用程序缓冲区的大小*/
正确方式
先对 length 进行处理,再去判断 length 的大小是否可以容纳下字符
size_t readline(int fd, char *buffer, size_t length) { char *buf_first = buffer; static char *buffer_pointer; int nleft = 0; static char read_buffer[512]; char c; //先对处理 while (--length> 0) { if (nleft <= 0) { int nread = recv(fd, read_buffer, sizeof(read_buffer), 0); if (nread < 0) { if (errno == EINTR) { length++; continue; } return -1; } if (nread == 0) return 0; buffer_pointer = read_buffer; nleft = nread; } c = *buffer_pointer++; *buffer++ = c; nleft--; if (c == ‘ ‘) { *buffer = ‘ ‘; return buffer - buf_first; } } return -1; }
面试题
1. 在读数据的时候,一般都需要给应用程序最终缓冲区分配大小,这个大小有什么讲究吗?
最终缓冲区的大小应该比预计接收的数据大小大一些,预防缓冲区溢出。
2. 例子中所分配的缓冲是否可以换成动态分配吗?比如调用 malloc 函数来分配缓冲区
完全可以动态分配,但是要记得在return前释放缓冲区
四次挥手理解
面试题
listen 函数中参数 backlog
CLOSE_WAIT
面试题
- 如果发现大量的CLOSE_WAIT状态,怎么解决?
以上是关于网络编程网络编程相关知识点总结1的主要内容,如果未能解决你的问题,请参考以下文章