一、网络架构
C/S (client/server 客户端/服务器):由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序,负责提供用户界面和交互逻辑 ,接收用户输入,向服务器发送请求,并展示服务器返回的结果;服务器端是提供服务的程序,一般部署在性能较强的计算机上,负责处理数据存储、业务逻辑计算等,监听特定端口等待客户端请求,接收到请求后进行处理并返回结果。
B/S(browser/server 浏览器/服务器):基于浏览器和服务器,客户端通过通用的浏览器(如 Chrome、Firefox、等)来访问应用程序。服务器端包括 Web 服务器和数据库服务器等,负责处理业务逻辑、存储和管理数据。
P2P(peer to peer 点对点):是一种去中心化的网络架构,网络中的节点(计算机)地位对等,不存在专门的中心服务器。每个节点既可以作为客户端向其他节点请求服务或资源,也可以作为服务器为其他节点提供服务或资源。
二、C/S 和 B/S 对比:
C/S架构 | B/S架构 | |
客户端 | 需要安装专用的客户端软件 | 无需安装客户端,直接通过浏览器访问 |
协议 | 可自定义私有协议(如游戏使用二进制协议),灵活性高。 | 基于HTTP/HTTPS 协议,需遵循 Web 标准(如 RESTful 接口)。 |
功能 | ||
资源 | 客户端分担部分计算压力,服务器专注数据处理,适合高并发、大数据量场景。 | 所有逻辑在服务器端执行,需应对大量 HTTP 请求,对服务器性能要求高。 |
三、C/S通信流程
服务器端流程
- 创建套接字(socket ()):调用
socket()
函数创建一个套接字,这是进行网络通信的基础。它会返回一个套接字描述符,后续操作将基于这个描述符进行。该函数确定通信的协议族(如 AF_INET 表示 IPv4)、套接字类型(如 SOCK_STREAM 表示 TCP 流套接字)和协议(通常为 0,由系统根据前两个参数自动选择合适协议)。- 绑定地址和端口(bind ()):使用
bind()
函数将创建的套接字与特定的 IP 地址和端口号绑定。这样服务器就能明确在哪个地址和端口上监听客户端的连接请求。需要提供套接字描述符、指向包含 IP 地址和端口号信息的结构体指针,以及该结构体的大小。- 监听连接(listen ()):调用
listen()
函数使服务器进入监听状态,它会为套接字创建一个等待连接的队列,参数包括套接字描述符和队列的最大长度。这一步告诉系统开始接受客户端的连接请求。- 接受连接(accept ()):
accept()
函数会阻塞(暂停执行),直到有客户端发起连接请求。一旦有客户端连接,它会创建一个新的套接字描述符用于与该客户端进行通信,同时返回客户端的地址信息。原来监听的套接字仍然保持监听状态,继续接受其他客户端的连接。- 读取数据(read ()):服务器通过新创建的与客户端通信的套接字描述符,使用
read()
函数读取客户端发送过来的数据。read()
函数从套接字接收数据并存储到指定的缓冲区中,返回实际读取的字节数。- 处理请求:服务器对读取到的数据进行相应的处理,例如解析请求内容、查询数据库、进行业务逻辑计算等。
- 写入数据(write ()):处理完请求后,服务器使用
write()
函数将处理结果(响应数据)通过套接字发送回客户端。write()
函数将缓冲区中的数据写入套接字,发送给客户端。- 再次读取数据(read ()):服务器可能再次调用
read()
函数,等待接收客户端后续可能发送的数据,比如新的请求或确认信息等。- 关闭连接(close ()):通信结束后,服务器调用
close()
函数关闭与客户端通信的套接字,释放相关资源。客户端流程
- 创建套接字(socket ()):与服务器端一样,客户端首先调用
socket()
函数创建一个套接字,用于后续的网络通信,返回套接字描述符。- 建立连接(connect ()):客户端使用
connect()
函数尝试与服务器建立连接。需要指定要连接的服务器的 IP 地址和端口号,以及套接字描述符。如果服务器处于监听状态并且接受连接,连接就会成功建立;否则可能会返回错误。- 写入数据(write ()):连接建立后,客户端调用
write()
函数向服务器发送请求数据,将请求内容写入套接字,发送给服务器。- 读取数据(read ()):客户端调用
read()
函数从套接字读取服务器返回的响应数据,将数据存储到指定的缓冲区中。- 关闭连接(close ()):客户端完成与服务器的通信后,调用
close()
函数关闭套接字,释放资源。整体交互过程
- 服务器端先完成初始化(创建套接字、绑定、监听),进入等待客户端连接的状态。
- 客户端创建套接字并尝试连接服务器,连接成功后,客户端向服务器发送请求数据。
- 服务器接收请求数据,处理后向客户端发送响应数据。
- 客户端接收响应数据,双方通信结束后各自关闭套接字,释放资源。
注意:pid(进程ID)只能在本机范围内发送,两台主机间无法直接发送
四、TCP通信的特点
- 有链接:并非一上来就直接进行传输,要先通过网络结点进行直接或者间接的连接,然后通过创建一些函数,连接起来(这条链路在通信过程中一直建立着)。
- TCP是一种可靠传输:
- 应答机制:在 TCP 通信里,每次接收方收到数据,都会给发送方发送一个应答报文(ACK) 。TCP 通过给每个数据段编号(序列号),接收方根据收到的数据段序列号,在应答报文中用确认序号告知发送方哪些数据已正确接收。例如发送方发送了编号为 1 - 1000 的字节数据,接收方若正确收到,就在应答报文中带上确认序号 1001(表示期望接收的下一个字节编号),告知发送方 1 - 1000 已正确接收 。
- 超时重传:发送方发送数据后,会设定一个超时时间,若在该时间内未收到接收方的 ACK 应答报文,不管是数据包丢失还是 ACK 确认应答丢失,发送方都认为数据传输失败,会重新发送数据 。比如网络拥堵导致数据包在传输途中滞留,超过超时时间仍未到达接收方,或者接收方发送的 ACK 在返回途中丢失,发送方都感知不到数据已被接收,就会触发超时重传
- 全双工:通信双方都具备独立的发送和接收通道,发送数据同时可接收数据。比如在网络通信中,网卡支持全双工,数据发送线和接收线各自独立工作 ;发送和接收操作瞬时同步进行。以电话通信为例,通话时双方说话和听到对方声音同步,语音信号在两个方向同时传输 。
- 连续:TCP 将数据视为无边界的连续字节流,发送方按顺序逐字节传输,接收方按序列号重组为连续的数据流。
- 有顺序:TCP 为每个字节数据分配唯一序列号,接收方按序列号重组数据,确保字节流顺序正确;接收方通过 ACK 报文告知发送方已收到的数据序号,发送方仅传输未确认的分组,避免乱序。(udp本身不保证)。
五、“三次握手四次挥手 ”
三次握手:
三次握手其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
在socket编程中,客户端执行connect()时,将触发三次握手:
- 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态 。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
- 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
- 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
四次挥手:
建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作,
刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下,在socket编程中,任何一方执行close()操作即可产生挥手操作:
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连 接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
- 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号 seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态, 客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待 2)状态,等待服务端发出的连接释放报文段。
- 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1, 序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户 端的确认。
- 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1, ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过
时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
特性: tcp也叫流式套接字
六、TCP服务端相关函数
socket() 创建套接字:
- 参数:
domain
:协议族(如AF_INET
表示 IPv4,AF_INET6
表示 IPv6)。type
:套接字类型(如SOCK_STREAM
表示 TCP 流式套接字,SOCK_DGRAM
表示 UDP 数据报套接字)。protocol
:通常为 0,表示自动选择对应协议(如 TCP 对应IPPROTO_TCP
)。- 返回值:成功返回套接字描述符(非负整数),失败返回
-1
并设置errno
。
bind() 绑定地址和端口 :
- 参数:
sockfd
:socket()
返回的套接字描述符。addr
:指向地址结构的指针(如struct sockaddr_in
)。addrlen
:地址结构的长度(如sizeof(struct sockaddr_in)
)。- 返回值:成功返回
0
,失败返回-1
。
listen() 监听连接:
- 参数:
sockfd
:已绑定的套接字描述符。backlog
:未处理连接队列的最大长度(如5
或SOMAXCONN
)。- 返回值:成功返回
0
,失败返回-1
。- 说明:将套接字从主动模式转为被动模式,等待客户端连接。
accept() 接受客户端连接:
- 参数:
sockfd
:监听套接字描述符(由listen()
创建)。addr
:存储客户端地址的结构体指针(可为NULL
)。addrlen
:地址结构体长度的指针(需初始化为结构体大小)。- 返回值:成功返回新的客户端套接字描述符,失败返回
-1
。- 说明:
- 阻塞直到有客户端连接到达。
- 返回的新套接字用于与客户端通信,原监听套接字继续监听
connect() 连接服务器:
- 参数:
sockfd
:客户端套接字描述符(由socket()
创建)。addr
:服务器地址结构体指针。addrlen
:地址结构体长度。- 返回值:成功返回
0
,失败返回-1
。- 说明:
- 客户端调用此函数发起与服务器的连接(触发三次握手)。
- 若连接失败(如服务器未监听),需重新调用
connect()
。
send() 发送数据:
- 参数:
sockfd
:已连接的套接字描述符。buf
:待发送数据的缓冲区指针。len
:数据长度(字节)。flags
:通常为0
,或设置特殊标志(如MSG_DONTWAIT
表示非阻塞)。- 返回值:成功返回实际发送的字节数,失败返回
-1
。- 说明:
- 数据可能未立即发送,而是存入发送缓冲区。
- 返回值可能小于
len
(如网络拥塞),需循环发送剩余数据。
recv() 接收数据 :
- 参数:
sockfd
:已连接的套接字描述符。buf
:存储接收数据的缓冲区指针。len
:缓冲区最大长度。flags
:通常为0
,或设置特殊标志(如MSG_PEEK
表示查看但不取出数据)。- 返回值:
- 成功返回实际接收的字节数。
- 返回
0
表示对方已关闭连接(FIN 包)。- 返回
-1
表示出错(如连接断开)。- 说明:
- 若无数据且未关闭连接,
recv()
默认阻塞。- 需循环读取直至数据全部接收(尤其对于大文件)。
close() 关闭套接字:
- 参数:
fd
:套接字描述符。- 返回值:成功返回
0
,失败返回-1
。- 说明:
- 关闭套接字并释放资源。
- 触发 TCP 四次挥手断开连接(若为主动关闭方)。
七、代码实现
- 服务端
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
int main(int argc, char** argv)
{
//监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd)
{
perror("socket");
return 1;
}
// man 7 ip
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
// 三次握手的排队数 ,
listen(listfd, 3);
socklen_t len = sizeof(cli);
//通信套接字
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn)
{
perror("accept");
return 1;
}
while (1)
{
char buf[256] = {0};
// ret >0 实际收到的字节数
//==0 表示对方断开
// -1 出错。
ret = recv(conn, buf, sizeof(buf), 0);
if(ret<=0)
{
break;
}
printf("cli:%s\n",buf);
time_t tm;
time(&tm);
struct tm * info = localtime(&tm);
sprintf(buf,"%s %d:%d:%d\n",buf, info->tm_hour,info->tm_min,info->tm_sec);
send(conn,buf,strlen(buf),0);
}
close(conn);
close(listfd);
// system("pause");
return 0;
}
- 客户端
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
int main(int argc, char** argv)
{
int conn = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == conn)
{
perror("socket");
return 1;
}
// man 7 ip
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(conn,(SA)&ser,sizeof(ser));
if(-1 == ret)
{
perror("connect");
return 1;
}
while (1)
{
char buf[256] = {0};
strcpy(buf,"this is tcp test");
send(conn,buf,strlen(buf),0);
ret = recv(conn, buf, sizeof(buf), 0);
if(ret<=0)
{
break;
}
printf("ser:%s",buf);
fflush(stdout);
sleep(1);
}
close(conn);
// system("pause");
return 0;
}
八、黏包问题
1.什么是粘包问题?
答:粘包问题是指在TCP通信中,发送方发送的多个独立消息在接收方被合并成一个消息接收的现象。换句话说,发送方发送的多条消息在接收方被“粘”在一起,导致接收方无法直接区分消息的边界。
2.粘包问题成因?
- TCP是面向流的协议,它将数据视为一个连续的字节流,不保留消息的边界。
- 发送方发送的多个消息可能被合并到同一个TCP包中发送。
- 接收方在读取数据时,无法直接知道哪些字节属于哪条消息。
3.粘包问题的影响?
- 接收方无法正确解析消息,可能导致数据解析错误。
- 系统的健壮性和可靠性降低,尤其是在需要严格消息边界的应用中。
4.如何解决?
- 添加分隔符:在每条消息末尾添加特殊分隔符(如
\n
或\r\n
),接收方通过分隔符来解析消息。 - 固定大小:每条消息的长度固定,接收方根据固定长度来解析消息。
- 自定义协议
九、练习
客户端-服务端 传送文件
- 服务端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
typedef struct
{
char filename[256];
char buf[1024];
int buf_len;
int total_len;
} PACK;
int main(int argc, char** argv)
{
//监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd)
{
perror("socket");
return 1;
}
// man 7 ip
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
// 三次握手的排队数 ,
listen(listfd, 3);
socklen_t len = sizeof(cli);
//通信套接字
int conn = accept(listfd, (SA)&cli, &len);
if (-1 == conn)
{
perror("accept");
return 1;
}
int first_flag = 0;
int fd = -1;
int current_size = 0;
int total_size = 0;
while (1)
{
PACK pack;
bzero(&pack, sizeof(pack));
ret = recv(conn, &pack, sizeof(pack), 0);
if (0 == first_flag)
{
first_flag = 1;
fd = open(pack.filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);
if (-1 == fd)
{
perror("open");
return 1;
}
total_size = pack.total_len;
}
if (0 == pack.buf_len)
{
break;
}
write(fd, pack.buf, pack.buf_len);
current_size += pack.buf_len;
printf("%d/%d\n", current_size, total_size);
bzero(&pack, sizeof(pack));
strcpy(pack.buf,"go on");
// send(conn,&pack,sizeof(pack),0);
}
close(conn);
close(listfd);
close(fd);
// system("pause");
return 0;
}
- 客户端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
typedef struct
{
char filename[256];
char buf[1024];
int buf_len;
int total_len;
} PACK;
int main(int argc, char** argv)
{
int conn = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == conn)
{
perror("socket");
return 1;
}
// man 7 ip
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(conn, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("connect");
return 1;
}
PACK pack;
strcpy(pack.filename, "1.png"); // /home/linux/1.png
struct stat st;
ret = stat("/home/linux/1.png", &st);
if (-1 == ret)
{
perror("stat");
return 1;
}
pack.total_len = st.st_size; // total size
int fd = open("/home/linux/1.png", O_RDONLY);
if (-1 == fd)
{
perror("open");
return 1;
}
while (1)
{
pack.buf_len = read(fd, pack.buf, sizeof(pack.buf));
send(conn, &pack, sizeof(pack), 0);
if (pack.buf_len <= 0)
{
break;
}
bzero(&pack,sizeof(pack));
//recv(conn,&pack,sizeof(pack),0);
usleep(1000*10); //10ms
}
close(conn);
close(fd);
// system("pause");
return 0;
}