什么是 TCP
TCP 是 Transmission Control Protocol(传输控制协议)的简写,意为"对数据传输过程的控制"。
TCP 套接字 是面向连接的,也被称为基于 流(stream) 的套接字。
TCP 原理
(一)TCP套接字中的 I/O 缓冲
之前了解到 TCP 套接字(面向连接的套接字)的数据收发无边界。服务器端即使调用1次 write
函数传输40 字节的数据,客户端也有可能通过4次 read
函数调用每次读取10字节。服务器端一次性传输了40字节,客户端却可以缓慢地分批接收。在客户端接收10字节后,剩下的30字节在那里等候呢?
实际上,write
函数调用后并非立即传输数据,read
函数调用后也并非马上接收数据。更准确的说,write
函数调用瞬间,数据将移至输出缓冲;read
函数调用瞬间,从输入缓冲读取数据。
I/O 缓冲特性可整理如下:
- I/O 缓冲在每个 TCP 套接字中单独存在。
- I/O 缓冲在创建套接字时自动生成。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲中的数据。
TCP 会控制数据流,内有滑动窗口(Sliding Window)协议,不会因为缓冲溢出而丢失数据,即不会发生超过输入缓冲大小的数据传输。
(二)TCP 内部工作原理
TCP 套接字从创建到消失的过程分为三步:
- ① 与对方套接字进行连接。
- ② 与对方套接字进行数据交换。
- ③ 断开与对方套接字的连接。
(1)与对方套接字的连接
TCP 在实际通信过程中会经过三次对话过程,该过程称为 "三次握手 "(Threee-way-handshaking),三次握手过程中,正式的数据传输并不开始。这三次握手的主要目的是建立一个可靠的连接,确保双方都准备好进行数据交换,并同步初始的序列号。 可简单描述为:
- SYN(同步)
- SYN-ACK(同步-确认)
- ACK(确认)
套接字 是以双工(Full-duplex)方式工作的,也就是说它可以双向传递数据。
三次握手的过程如下:
① 请求连接的主机 A 向主机 B 传递消息:[SYN] SEQ: 1000, ACK: -
SEQ
为1000,含义如下:
"现传递的数据包序号为1000,如果接收无误,请通知我向您传递1001号数据包"
这是首次请求连接使用的消息,SYN
是 Synchronization 的缩写,表示收发数据前传输的同步消息。
② 主机 B 向主机 A 传递消息:[SYN+ACK] SEQ: 2000, ACK: 1001
SEQ
为2000,含义如下:
"现传递的数据包序号为2000,如果接收无误,请通知我向您传递2001号数据包"
ACK
为1001,含义如下:
"刚才传输的 SEQ 为1000的数据包接收无误,现在请传递 SEQ 为1001的数据包"
③ 主机 A 向主机 B 传递消息:[ACK] SEQ: 1001, ACK: 2001
SEQ
为1001,含义如下:
"现传递的数据包序号为1001,如果接收无误,请通知我向您传递1002号数据包"
ACK
为2001,含义如下:
"刚才传输的 SEQ 为2000的数据包接收无误,现在可以传递 SEQ 为2001的数据包"
至此,主机 A 和主机 B 确认了彼此均就绪。
收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所作的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。
(2)与对方主机进行数据交换
数据收发过程
通过第一步三次握手过程完成了数据交换准备,下面开始正式收发数据,其默认方式如下图所示:
上图为主机 A 分2次(2个数据包)向主机 B 传递200字节的过程。
首先主机 A 通过1个数据包发送100个字节的数据,数据包的 SEQ 为1200。主机 B 为了确认这一点,向主机 A 发送 ACK 1301 消息。
此时 ACK 为1301而非1201,是因为 ACK 号的增量为传输的数据字节,若 ACK 不加传输的字节数,虽然也可以确认数据包的传输,但无法明确数据全部传递还是丢失了一部分。所以按如下公式传递 ACK 消息:
ACK 号 = SEQ 号 + 传递的字节数 + 1
其中加 1 是为了告知对方下次要传递的 SEQ 号。
数据包消失的情况
通过 SEQ 1301数据包向主机 B 传递100字节的过程中发生错误,主机 B 未收到。经过一段时间,主机 A 仍未收到对于 SEQ 1301的 ACK 确认,因此试着重传该数据包。
为了完成数据包重传,TCP 套接字启动计时器以等待 ACK 应答,若相应计时器发生超时(Time-out!)则重传。
(3)断开与套接字的连接
断开套接字连接的过程称为 "四次握手"。
① FIN(客户端发起关闭请求)
② ACK(服务器确认关闭请求)
③ FIN(服务器发起关闭请求)
④ ACK(客户端确认关闭请求)
TCP/IP 协议栈
(一)层次结构
TCP/IP 协议栈共分4层,可以理解为数据收发分成了4个层次化过程:
通过 TCP 套接字收发数据时需要借助下面4层:
(二)各层的功能
(1)链路层
链路层负责在物理网络上传输数据帧,它处理物理介质(如以太网、Wi-Fi)的传输细节,专门定义 LAN、WAN、MAN 等网络标准。若两台主机通过网络进行数据交换,需要如下图所示的物理连接,链路层就负责这些标准。
(2)IP 层
准备好物理连接之后就要传输数据。为了在复杂的网络中传输数据,首先需要考虑路径的选择。向目标传输数据需要经过哪条路径? 解决此问题的就是 IP 层,该层使用的协议就是 IP(Internet Protocol)。
IP 本身是面向消息的,不可靠的协议。每次传输数据时会帮我们选择路径,但并不一致。如果传输中发生路径错误,则选择其他路径,但如果发生数据丢失或错误,则无法解决,即IP协议无法应对数据错误。
-
面向消息:IP 协议是面向数据报(datagram)的协议,也就是说,它处理的数据单位是数据报。每个数据报都被独立地处理和转发,没有整体的会话或连接概念。
-
不可靠的协议 :IP 协议本身不提供数据传输的可靠性。它不会保证数据包的送达、顺序或完整性。IP 只负责将数据包从源地址传送到目标地址,但不对数据的丢失、重复或错误进行修正。
-
路径选择 :IP 协议会根据当前的网络状况选择合适的路径将数据包发送到目标地址。由于网络情况可能变化,数据包可能会通过不同的路径到达目的地。
-
错误处理 :IP 协议不处理数据传输中的错误。如果在传输过程中发生数据丢失、错误或重复,IP协议不会自动纠正这些问题。处理这些问题通常是由上层协议(如 TCP)来完成的。TCP 在 IP 协议之上提供了可靠的数据传输服务,包括数据重传、错误检测和流量控制等功能。
(3)TCP/UDP 层
IP 层解决数据传输中的路径选择问题, TCP/UDP 层以 IP 层提供的路径信息为基础完成实际的数据传输 ,所以该层又称为传输层(Transport)。本章节内容主要介绍 TCP 的作用:
如果数据交换过程中可以确认对方已收到数据,确保数据按顺序送达,并重传丢失的数据,那么即使 IP 层不保证数据传输,这类通信可是可靠的,这就是 TCP 的作用。
(4)应用层
应用层直接与用户应用程序交互,提供网络服务和应用协议。
编写软件的过程中,需要根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这就是应用层协议。网络编程的大部分内容就是设计并实现应用层协议。
基于 TCP 的服务器端/客户端
(一)TCP 服务器端默认函数调用顺序
其中 socket()
函数和 bind()
函数之前已经解释过,下面解释其他几个函数。
(1)listen()
函数:进入等待连接请求状态
调用 bind()
函数给套接字分配地址后,就要通过调用 listen()
函数进入等待连接请求状态。只有调用了 listen()
函数,客户端才能调用 connect()
函数(若提前调用将发生错误)。
c
#include <sys/socket.h>
// 成功时返回0, 失败时返回-1
int listen(int sock, int backlog);
sock
:希望进入连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器套接字(监听套接字)。backlog
:连接请求等待队列(Queue)的长度,若为5,表示最多使5个连接请求进入队列。
(2)accept()
函数:受理客户端连接请求
调用 listen()
函数后,若有新的连接请求,则应按序处理。受理请求意味着进入可接受数据的状态。
进入这种状态需要另一个套接字 ,accept()
函数将自动创建这个套接字,并连接到发起请求的客户端。
c
#include <sys/socket.h>
// 成功时返回创建的套接字文件描述符,失败时返回-1
int accept(int sock, struct sockaddr* addr, socken_t* addrlen);
sock
:服务器套接字的文件描述符。addr
:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。addrlen
:第二个参数 addr 结构体的长度,是一个地址。
函数调用成功时,accept
函数内部将产生用于数据 I/O 的套接字,并返回其文件描述符。
(二)Hello World 服务器端代码详解
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello World!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 创建套接字,但此时的套接字尚非真正的服务器端套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
// 初始化结构体变量
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
// 调用bind函数
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
error_handling("bind() error");
// 调用listen函数进入等待连接请求状态,请求等待队列长度设为5。
// 此时的套接字才是服务器端套接字
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
// 调用accept函数从队头取一个连接请求与客户端建立连接,并返回创建的套接字文件描述符。
// 若调用 accept 函数时等待队列为空,则accept函数不会返回,直到队列中出现新的客户端连接
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
if(clnt_sock==-1)
error_handling("accept() error");
// 传输数据
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
// 关闭连接
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(三)TCP 客户端默认函数调用顺序
与服务器端相比,区别在于 "请求连接",它是创建客户端套接字后向服务器端发起的连接请求,连接请求通过调用 accept()
函数完成。
c
#include <sys/socket.h>
// 成功时返回0,失败时返回-1
int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);
sock
:客户端套接字文件描述符。servaddr
:保存目标服务器端地址信息的变量地址值。addrlen
:是servaddr
指向的地址结构体的大小,以字节为单位。
客户端调用 connect
函数后,发生以下情况之一才会返回(完成函数调用)。
① 服务器端接收连接请求。
② 发生断网等异常情况而中断连接请求。
(四)Hello World 客户端代码详解
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 创建套接字
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
// 初始化IP和端口信息(目标服务器套接字的IP和端口信息)
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
// 调用connect函数向服务器端发送连接请求
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error!");
// 完成连接后,接收服务器端传输的数据
str_len=read(sock, message, sizeof(message)-1);
if(str_len==-1)
error_handling("read() error!");
printf("Message from server: %s \n", message);
// 结束与服务器端的连接
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(五)基于 TCP 的服务器端/客户端的函数调用关系
实现处理多请求的服务器端/客户端
之前讨论的 Hello World 服务器端处理完1个客户端连接请求就退出,连接请求等待队列实际没有太大意义,如果想继续受理后续的客户端连接请求,最简单的办法就是插入循环语句反复调用 accept
函数。
接下来实现处理多请求的回声服务器端 及其配套的回声客户端,这里的"回声"是指服务器端将客户端传输的字符串数据原封不动传回客户端。首先整理一下程序基本运行方式:
- 服务器端在同一时刻只与一个客户端相连。
- 服务器端依次向5个客户端提供服务并退出。
- 客户端接收用户输入的字符串并发送到服务器端。
- 服务器端将接收的字符串数据传回客户端。
- 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。
(一)Linux 平台服务器端代码(echo_server.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
// 为处理5个客户端连接而添加的循环语句
for(i=0; i<5; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
// 实际完成回声服务的代码,原封不动的传输读取的字符串
// 若客户端套接字调用close函数,跳出循环
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(二)Linux 平台客户端代码(echo_client.c)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(三)运行结果
echo_server.c:
echo_client.c:
(四)基于 Windows 的实现
为了完成平台之间转换,需要记住以下4点:
① 通过 WASStartup
、WSACleanup
函数初始化并清除套接字相关库。
② 把数据类型和变量名切换为 Windows 风格。
③ 数据传输中用 recv
、send
而非 read
、write
函数。
④ 关闭套接字时用 closesocket
函数而非 close
函数。
(1)服务器端
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
char message[BUF_SIZE];
int strLen, i;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSize;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hServSock=socket(PF_INET, SOCK_STREAM, 0);
if(hServSock==INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(atoi(argv[1]));
if(bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hServSock, 5)==SOCKET_ERROR)
ErrorHandling("listen() error");
clntAdrSize=sizeof(clntAdr);
for(i=0; i<5; i++)
{
hClntSock=accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
if(hClntSock==-1)
ErrorHandling("accept() error");
else
printf("Connected client %d \n", i+1);
while((strLen=recv(hClntSock, message, BUF_SIZE, 0))!=0)
send(hClntSock, message, strLen, 0);
closesocket(hClntSock);
}
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(2)客户端
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
char message[BUF_SIZE];
int strLen;
SOCKADDR_IN servAdr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hSocket=socket(PF_INET, SOCK_STREAM, 0);
if(hSocket==INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
send(hSocket, message, strlen(message), 0);
strLen=recv(hSocket, message, BUF_SIZE-1, 0);
message[strLen]=0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(五)回声客户端存在的问题
上述客户端是基于 TCP 的,然而 TCP 不存在数据边界,所以存在问题:
问题
如果数据太大,操作系统可能将数据分成多个数据包发送到客户端,客户端有可能在尚未全部收到全部数据包时就调用 read
函数。
解决办法
因为可以提前确定确定接收数据的大小,所以如果之前传输了20字节的字符串,则在接收时循环调用 read
函数读取20个字节即可。
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len, recv_len, recv_cnt;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
str_len=write(sock, message, strlen(message));
recv_len=0;
while(recv_len<str_len)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt==-1)
error_handling("read() error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
问答
(1)客户端中为何不需要调用 bind
函数分配地址?如果不调用 bind
函数,那何时、如何向套接字分配IP地址和端口号?
如何分配 IP 地址和端口号
-
自动分配:
- 客户端在连接到服务器时,不需要显式地绑定到一个特定的端口。调用
connect
函数时 客户端的操作系统会自动为客户端套接字分配一个临时端口号和适当的 IP 地址。客户端通过connect
函数向服务器发送连接请求,不需要自己管理这些细节。
- 客户端在连接到服务器时,不需要显式地绑定到一个特定的端口。调用
-
端口号范围:
- 操作系统从一个动态端口号范围中选择一个端口。这个范围通常是 49152 到 65535(具体范围可能因操作系统而异)。选择的端口号在客户端套接字的生命周期内是唯一的,避免了与其他进程的端口冲突。
客户端使用 bind
函数的情况
尽管在客户端编程中通常不需要显式调用 bind
,但有一些特殊情况下,你可能会使用 bind
函数:
-
指定本地端口:
- 如果客户端需要使用特定的端口号,可以调用
bind
函数来指定。这样做通常用于调试或满足某些特定的需求。
- 如果客户端需要使用特定的端口号,可以调用
-
多重套接字:
- 在某些应用场景中,一个客户端可能同时需要维护多个连接。为了管理这些连接,可能会在客户端上使用多个套接字,并为每个套接字分配不同的端口号。
(2)TCP 套接字中调用 write
函数和 read
函数时数据如何移动?结合I/O缓冲进行说明。
-
写操作 :
write
函数将数据从用户态缓冲区移动到内核的发送缓冲区。 -
发送操作:数据从发送缓冲区经过网络发送到对端主机。
-
接收操作:对端主机的数据被放入接收缓冲区。
-
读操作 :
read
函数将数据从接收缓冲区移动到用户态缓冲区。