一.概念
网络编程是指通过计算机网络进行数据交换的过程。它涉及创建网络应用程序,使不同的计算机能够相互通信。网络编程的核心是使用套接字(Socket)进行数据传输,所以有时我们也直接叫socket编程
1.网络通信协议
在学习这个之前我们先学习一下几种网络模型
- OSI模型:七层结构,每一层都有详细的功能定义,但实际应用中较少使用。
- TCP/IP模型:四层结构,是互联网的基础,实际应用广泛。
- 五层模型:结合了OSI模型和TCP/IP模型的优点,简化了OSI模型,同时包含了TCP/IP模型的核心内容,是教学和理解网络通信的常用模型。
最常用的还是4层模型(下一节会对其展开描述),而本文开头所讲的socket就是位于应用层和传输层之间的一个接口。它提供了一种编程接口,使得应用程序可以通过网络进行通信。
2. TCP/IP协议
TCP/IP协议(Transmission Control Protocol/Internet Protocol)是互联网通信的基础协议,它定义了数据如何在网络中进行传输和路由。TCP/IP协议栈包括四个主要层次,每个层次负责不同的功能。
2.1TCP/IP协议栈的四个层次
-
应用层(Application Layer)
- 负责处理特定的应用程序细节。
- 常见协议:HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)、DNS(域名系统)等。
-
传输层(Transport Layer)
- 负责端到端的可靠数据传输。
- 主要协议:TCP(传输控制协议)和UDP(用户数据报协议)。
- TCP提供可靠的、面向连接的服务,确保数据按顺序到达。
- UDP提供不可靠的、无连接的服务,适用于实时应用。
-
网络层(Internet Layer)
- 负责数据包的路由和转发。
- 主要协议:IP(互联网协议)。
- IP负责将数据包从源地址传输到目的地址。
-
链路层(Link Layer)
- 负责在物理网络(如以太网、Wi-Fi)上传输数据。
- 主要协议:以太网协议、Wi-Fi协议等。
2.2 TCP/IP数据传输过程
- 应用层:应用程序生成数据,并通过应用层协议(如HTTP)进行封装。
- 传输层:数据被分割成数据段,并添加TCP或UDP头部。
- 网络层:数据段被封装成数据包,并添加IP头部。
- 链路层:数据包被封装成帧,并添加链路层头部和尾部。
- 物理层:帧通过物理介质(如电缆、光纤)传输。
2.3 tcp的”三次握手“和”四次挥手“
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
- 第一次握手:客户端向服务器发送一个SYN(同步)报文,请求建立连接。此时,客户端进入SYN_SENT状态。
- 第二次握手:服务器收到SYN报文后,返回一个SYN-ACK(同步-确认)报文,表示服务器同意建立连接。此时,服务器进入SYN_RECEIVED状态。
- 第三次握手:客户端收到SYN-ACK报文后,发送一个ACK(确认)报文给服务器,确认连接已建立。此时,客户端和服务器都进入ESTABLISHED状态,连接正式建立。
- 第一次挥手:客户端向服务器发送一个FIN(结束)报文,请求释放连接。此时,客户端进入FIN_WAIT_1状态。
- 第二次挥手:服务器收到FIN报文后,返回一个ACK报文,确认收到客户端的释放请求。此时,服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
- 第三次挥手:服务器处理完所有必要的数据后,向客户端发送一个FIN报文,请求释放连接。此时,服务器进入LAST_ACK状态。
- 第四次挥手:客户端收到FIN报文后,返回一个ACK报文,确认收到服务器的释放请求。此时,客户端进入TIME_WAIT状态,服务器收到ACK后进入CLOSED状态。经过一段时间后,客户端也进入CLOSED状态,连接正式释放
2.4 为什么不能是两次握手
在建立TCP连接的过程中,三次握手步骤不可或缺,其核心目的在于对双方的数据通信能力进行彻底的确认。
- 第一次握手:客户端发起连接请求,发送一个包含SYN(同步序列编号)标志的数据包。这一步骤不仅检验了客户端的发送功能,也验证了服务端的接收能力。
- 第二次握手:服务端响应,发送一个包含SYN和ACK(确认)标志的数据包,确认收到客户端的请求,并反馈自己的接收和发送状态。然而,此时服务端尚无法确认客户端的接收能力。
- 第三次握手:客户端再次发送一个包含ACK标志的数据包,确认服务端的响应。这一动作使得服务端得以确认客户端的接收和发送功能均正常,同时自己也完成了对双方通信能力的全面验证。
因此,三次握手不仅是确认双方通信能力的关键,也是确保数据传输无误、连接稳定的重要保障。简而言之,仅通过两次握手无法达到同样的目的,因为无法完全验证双方的接收和发送能力。
2.5 为什么不能是三次挥手
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
2.6 UDP
提供无连接的、不可靠的数据报服务。适用于对速度要求高,但对数据完整性要求不高的场景,如视频流、语音通信等。
二.socket编程
在前面的时候就已经多次提到了socket编程,Socket编程是网络编程中的一个重要概念,它允许不同主机上的进程进行通信,可以把socket想象成一个管道,通过它可以实现数据的传输,所以这个一般是成对的出现的,从一个socket输入,然后从另外一个socket接收信息。
1.socket开发流程(服务器)
1. 创建Socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
AF_INET
:指定使用IPv4地址。SOCK_STREAM
:指定使用TCP协议。0
:通常用于指定协议,对于TCP,这个值通常是0。
2. 准备服务器地址结构
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 允许绑定到任何IP地址
servaddr.sin_port = htons(8080); // 端口号
sockaddr_in
:IPv4地址结构。memset
:清零地址结构。htonl
和htons
:主机到网络字节顺序转换。
3. 绑定Socket到地址
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
bind
:将Socket绑定到指定的地址和端口。
4. 监听连接
listen(sockfd, 10); // 第二个参数是队列长度
listen
:使Socket变为监听状态,允许指定等待连接的最大数量。
5. 接受连接
struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int newsockfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
accept
:等待并接受客户端的连接请求。
6. 读写数据
char buffer[1024];
int n = read(newsockfd, buffer, sizeof(buffer));
if (n > 0) {
write(newsockfd, buffer, n);
}
read
:从Socket读取数据。write
:向Socket写入数据。
7.关闭连接
close(newsockfd);
close(sockfd);
close
:关闭Socket。
2.socket开发流程(客户端)
1. 创建Socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
AF_INET
:指定使用IPv4地址。SOCK_STREAM
:指定使用TCP协议。0
:通常用于指定协议,对于TCP,这个值通常是0。
2.连接指定计算机的端口
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:这是已经创建好的套接字描述符。addr
:这是一个指向sockaddr
结构的指针,该结构包含了要连接的服务器的地址信息。addrlen
:这是一个表示addr
指向的结构大小的socklen_t
类型变量。
3.向socket中读写信息
char buffer[1024];
int n = read(newsockfd, buffer, sizeof(buffer));
if (n > 0) {
write(newsockfd, buffer, n);
}
read
:从Socket读取数据。write
:向Socket写入数据。
4.关闭套接字
close(sockfd);
close
:关闭Socket。
三.TCP 客户端服务端通信
服务器
// server
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
int main(int argc, char const *argv[])
{
// 创建套接字
// int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 服务地址
// struct sockaddr_un addr;
// addr.sun_family = AF_UNIX;
// strcpy(addr.sun_path, "my_server2");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000); // 端口号
// 0.0.0.0
addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
// inet_addr 函数默认执行了 主机字符序列到网络字符序列的转换
// addr2.sin_addr.s_addr = inet_addr("127.0.0.1");
// addr2.sin_addr.s_addr = htonl(INADDR_ANY);
// addr2.sin_port = htons(9000);
// 绑定地址
bind(server_fd, (struct sockaddr *) &addr, sizeof(addr));
// 监听
listen(server_fd, 9);
printf("服务器启动\n");
// struct sockaddr_un addr_client;
// addr_client.sun_family = AF_UNIX;
// int addr_len = sizeof(addr_client);
struct sockaddr_in c_addr;
int c_addr_len = sizeof(c_addr);
// 子进程结束
// 忽略
signal(SIGCHLD, SIG_IGN);
while (1)
{
// 接收请求,建立了 TCP 连接,获得了一个新的客户端套接字
printf("等待客户端请求...\n");
int client_fd = accept(
server_fd,
(struct sockaddr *)&c_addr,
&c_addr_len);
// 并发:进程、线程
// 异步
if (fork() == 0)
{
int pid = getpid();
// 读写数据
char buf[32];
for (size_t i = 0; i < 9; i++)
{
read(client_fd, &buf, 32);
printf("%d 收到 %ld:%s\n", pid,i, buf);
// TODO
char msg[32] ;
fgets(msg, 32, stdin);
write(client_fd, &msg, 32);
}
// 退出
exit(EXIT_SUCCESS);
}
else
{
close(client_fd);
}
}
return 0;
}
客户端
// client
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
// 创建套接字
// 1. 域
// 2. 套接字类型
// 3. 协议
// int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
// 服务器地址
// struct sockaddr_un addr;
// addr.sun_family = AF_UNIX;
// strcpy(addr.sun_path, "my_server2");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
// 服务器
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// addr.sin_port = 9000;
// 连接服务器
// 1. 套接字
// 2. 目标地址
// 3. 目标地址长度
int r = connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr));
if (r == -1)
{
printf("连接失败\n");
return EXIT_FAILURE;
}
char ch[32];
for (size_t i = 0; i < 9; i++)
{
fgets(ch, 32, stdin);
ch[strcspn(ch, "\n")] = '\0';
// 读写数据
write(socket_fd, &ch, sizeof(ch)-1);
printf("已发送数据 %ld\n", i);
read(socket_fd, &ch, sizeof(ch));
printf("接收到:%s\n", ch);
}
// 关闭套接字
close(socket_fd);
return 0;
}
这是一段简单的服务器和客户端代码,演示了基于TCP的网络通信,客户端从标准输入读取数据,发送到服务器,并等待服务器的响应,然后打印出接收到的数据。
好的到这里本期的内容就结束了,如有错误,欢迎大家批评指正,感谢您的观看