Github源码
moranzcw/Computer-Networking-A-Top-Down-Approach-NOTES: 《计算机网络-自顶向下方法(原书第6版)》编程作业,Wireshark实验文档的翻译和解答。 (github.com)
暂定打算分2步走,前置是中科大对应计网黑书的视频
第1步完成14个WireShark实验,5个课后实验(共19个计网实验)
第2步,还有时间的话,cs144的实验跟一遍,没时间就直接开始tinyWebserver
目录
🌹关于 Socket
💧预习 TCP
🌼概念
🌼原理
🌼结构体
🌼部分代码
(1)服务器的Socket编程 -- sockaddr_in
(2)客户端的Sockets编程 -- hostent
💧预习 UDP
(1)客户端 UDP Socket
(2)服务器 UDP Socket
🔥TCP套接字(实验1)
(1)过程
(2)代码
(3)详细解释
🎂服务器
🎂客户端
🔥UDP套接字(实验2)
🌹关于 Socket
Socket,即“套接字”,是计网中通信的一种机制。
它提供的接口,使得app不用关心底层协议的细节。
(1)功能上
实现进程间数据传输,不同计算机的交互。
可用于实现 C / S 模型(client / server)(客户端 / 服务器)
实现对等通信模型(P2P)
(2)网络层次上
位于应用层和传输层之间。
它通过封装传输层的协议(TCP / UDP),为应用层提供统一的接口。
将应用层数据包装成传输层数据报文,发送到网络,
同时也接受传输层的数据报文,解析后交给应用层处理。
(3)协议栈上
Socket是基于网络协议栈的接口,位于app与OS内核之间。
app通过Socket API调用函数,向OS内核发送请求,
内核根据Socket API指定的网络协议(TCP / UDP),使用对应协议栈完成传输和接受。
💧预习 TCP
🌼概念
socket 原意是插口,每个插口对应一个编号
插口就是 socket 服务(插孔对应的编号就是端口号,插头也是一个 socket 服务)
Socket(套接字)到底是什么呢?👇
(1)所以,socket 即两个应用程序通过一个双向的通信连接实现数据交换,连接的这一段就是 socket(又称 套接字)
(2)套接字:网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个 套接字,就是网络上进程通信的一端(上联应用进程,下联网络协议栈)
(3)进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点
(4)由 IP地址 和 端口 结合,提供向应用层进程传送数据包的机制
实现一个 socket 连接通信至少需要 2 个套接字,一个运行在服务端(插孔),一个运行在客户端(插头)
Socket 是应用层 与 传输层(TCP/IP协议簇)的抽象中间层
不同于OSI模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中。
- 应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
- 传输层:TCP,UDP
- 网络层:IP,ICMP,OSPF,EIGRP,IGMP
- 数据链路层:SLIP,CSLIP,PPP,MTU
Socket是什么 - 简书 (jianshu.com)
【网络编程知识】什么是Socket?概念及原理分析-云社区-华为云 (huaweicloud.com)
🌼原理
场景
打电话给女朋友要先拨号,女朋友听到响铃会拿起电话,此时按时你就和女朋友建立了连接,就可以通话了。
交流完后,挂断电话结束本次交流。
原理
漏了个,客户端 close()后,服务端还有个 read(),然后服务端最后 close()
“3次握手,4次挥手”及相关细节补充👇
TCP和UDP的传输过程以及二者之间的区别_数据通信分tcp和udp,属于什么过程-CSDN博客
TCP和UDP的区别 - notes (gitbook.io)
🌼结构体
数据结构1
数据结构2
🌼部分代码
(以下是GPT给的代码,Windows下,codeblocks里无法运行 --
-- 需要到Linux下运行。此处只是对原理进行代码解释)
(1)服务器的Socket编程 -- sockaddr_in
struct sockaddr_in {
short sin_family; // 地址族,一般为AF_INET(IPv4)
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字段,通常不需要使用
};
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 创建socket
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务器地址和端口
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET; // 地址族为IPv4
serverAddress.sin_addr.s_addr = INADDR_ANY; // 使用本地任意可用IP地址
serverAddress.sin_port = htons(8080); // 监听8080端口
// 绑定socket到服务器地址和端口
bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
// 监听连接请求
listen(serverSocket, 5);
std::cout << "Server started. Waiting for connections..." << std::endl;
while (true) {
// 接受客户端连接
sockaddr_in clientAddress{};
socklen_t addrSize = sizeof(clientAddress);
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, &addrSize);
std::cout << "New connection accepted" << std::endl;
// 读取客户端发来的数据
char buffer[1024];
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
std::cout << "Received message: " << buffer << std::endl;
// 向客户端发送响应数据
const char* response = "Hello from server!";
send(clientSocket, response, strlen(response), 0);
// 关闭客户端连接
close(clientSocket);
}
// 关闭服务器socket
close(serverSocket);
return 0;
}
(2)客户端的Sockets编程 -- hostent
struct hostent {
char* h_name; // 官方名称
char** h_aliases; // 别名列表
int h_addrtype; // 地址类型(一般为AF_INET)
int h_length; // 地址长度(一般为4字节)
char** h_addr_list; // IP地址列表
};
#include <iostream>
#include <netdb.h>
#include <arpa/inet.h>
int main() {
const char* hostname = "www.example.com";
// 通过主机名获取hostent结构体
struct hostent* host = gethostbyname(hostname);
if (host == nullptr) {
std::cerr << "Failed to get host information" << std::endl;
return 1;
}
// 输出主机名和别名
std::cout << "Hostname: " << host->h_name << std::endl;
for (char** alias = host->h_aliases; *alias != nullptr; ++alias) {
std::cout << "Alias: " << *alias << std::endl;
}
// 输出IP地址
for (char** address = host->h_addr_list; *address != nullptr; ++address) {
sockaddr_in* addr = reinterpret_cast<sockaddr_in*>(*address);
std::cout << "IP Address: " << inet_ntoa(addr->sin_addr) << std::endl;
}
return 0;
}
💧预习 UDP
UDP是面向无连接的协议,不需要像TCP一样建立连接和关闭连接。因此,UDP服务器和客户端都只需分别创建一个socket,并通过
sendto()
和recvfrom()
发送和接收数据,不需要调用accept()
和bind()
函数
代码源于GPT,需要在Linux下运行 (此处只供学习,不一定能跑通)
结构体👇
struct sockaddr_in {
short sin_family; // 地址族,一般为AF_INET(IPv4)
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 填充字段,通常不需要使用
};
(1)客户端 UDP Socket
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 创建socket
int clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
// 设置服务器地址和端口
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET; // 地址族为IPv4
serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址
serverAddress.sin_port = htons(8080); // 服务器端口号
// 发送消息到服务器
const char* message = "Hello from client!";
sendto(clientSocket, message, strlen(message), 0, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
// 接收服务器响应
char buffer[1024];
ssize_t bytesRead = recvfrom(clientSocket, buffer, sizeof(buffer), 0, nullptr, nullptr);
std::cout << "Received response: " << buffer << std::endl;
// 关闭客户端socket
close(clientSocket);
return 0;
}
(2)服务器 UDP Socket
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 创建socket
int serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
// 设置服务器地址和端口
sockaddr_in serverAddress{};
serverAddress.sin_family = AF_INET; // 地址族为IPv4
serverAddress.sin_addr.s_addr = INADDR_ANY; // 使用本地任意可用IP地址
serverAddress.sin_port = htons(8080); // 监听8080端口
// 绑定socket到服务器地址和端口
bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
std::cout << "Server started. Waiting for messages..." << std::endl;
while (true) {
// 接收客户端消息
char buffer[1024];
sockaddr_in clientAddress{};
socklen_t addrSize = sizeof(clientAddress);
ssize_t bytesRead = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (struct sockaddr*)&clientAddress, &addrSize);
std::cout << "Received message from " << inet_ntoa(clientAddress.sin_addr) << ":" << ntohs(clientAddress.sin_port) << std::endl;
std::cout << "Message: " << buffer << std::endl;
// 向客户端发送响应数据
const char* response = "Hello from server!";
sendto(serverSocket, response, strlen(response), 0, (struct sockaddr*)&clientAddress, addrSize);
}
// 关闭服务器socket
close(serverSocket);
return 0;
}
🔥TCP套接字(实验1)
(1)过程
1,vscode先配置py环境
2,搞乱了需要彻底重装:如何完全重置或卸载vscode_哔哩哔哩_bilibili
3,Github源码git clone到本地,解压缩,并导入vscode👇
4,打开cmd,ipconfig查看ip 地址,找到IPv4的ip地址,作为代码中的 ip 地址
5,新打开2个cmd
对应目录下运行,服务器和客户端代码👇
可以看到,第一次,ConnectionRefused,这时我们需要打开👇
网络共享 - Internet选项 - 连接 - 局域网设置 - 自动检测设置(✔)
Python报错:ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。_有无目标计算机积极拒绝,无法连接网络-CSDN博客
Client
Server
可是当我第2次TCP时,又拒绝连接,再次python运行才成功,也许有一定几率拒绝?👇
(2)代码
TCPClient.py
from socket import *
serverName = '192.168.15.1' # 本地IPv4地址,ipconfig得到
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_STREAM) # 建立TCP套接字,使用IPv4协议
clientSocket.connect((serverName,serverPort)) # 向服务器发起连接
sentence = input('Input lowercase sentence:').encode() # 用户输入信息,并编码为bytes以便发送
clientSocket.send(sentence) # 将信息发送到服务器
modifiedSentence = clientSocket.recvfrom(1024) # 从服务器接收信息
print(modifiedSentence[0].decode()) # 显示信息
clientSocket.close() # 关闭套接字
TCPServer.py
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_STREAM) # 创建TCP欢迎套接字,使用IPv4协议
serverSocket.bind(('',serverPort)) # 将TCP欢迎套接字绑定到指定端口
serverSocket.listen(1) # 最大连接数为1
print("The server in ready to receive")
while True:
connectionSocket, addr = serverSocket.accept() # 接收到客户连接请求后,建立新的TCP连接套接字
print('Accept new connection from %s:%s...' % addr)
sentence = connectionSocket.recv(1024) # 获取客户发送的字符串
capitalizedSentence = sentence.upper() # 将字符串改为大写
connectionSocket.send(capitalizedSentence) # 向用户发送修改后的字符串
connectionSocket.close() # 关闭TCP连接套接字
(3)详细解释
🎂服务器
1,关于端口号 port
常用的端口号范围是从0到65535,其中0到1023是被保留给一些特定的服务(如HTTP的端口80、HTTPS的端口443等),而1024到65535是可供自由使用的端口范围
端口号用于标识特定的应用程序或服务
当您编写服务器端代码时,可以通过指定一个特定的端口号来监听客户端的连接请求
常见的做法是选择一个未被常用服务占用的端口号,例如12000、8080
在服务器端代码中指定一个合适的端口号来监听连接请求,然后在客户端程序中使用相同的端口号来连接服务器。这样,客户端和服务器之间就可以通过指定的端口号进行通信。
2, 详细流程
在目录 - 💧预习TCP - 🌼原理,进行回顾
3,
serverSocket = socket(AF_INET, SOCK_STREAM) # 创建TCP欢迎套接字,使用IPv4协议
socket()
函数是用来创建套接字的系统调用。它接受两个参数:地址族(Address Family)和套接字类型(Socket Type)。
AF_INET
参数指定了使用 IPv4 地址族,表示将使用 IPv4 地址来进行网络通信。
SOCK_STREAM
参数指定了套接字的类型为流式套接字(TCP),它提供了可靠的、面向连接的、基于字节流的数据传输。
serverSocket
是一个变量,用于存储创建的服务器端套接字
套接字,即 ip 地址和 端口号的整合。ip 地址标识主机,端口号表示应用程序或通信端口。
4,
serverSocket.bind(('',serverPort)) # 将TCP欢迎套接字绑定到指定端口
bind()
函数是用来将套接字绑定到一个特定的地址和端口号上的系统调用。它接受1个参数:IP 地址和端口号 的集合。
''
参数表示将服务器端套接字绑定到所有可用的网络接口上。这样,服务器就可以监听来自任何 IP 地址的客户端连接请求。
serverPort
参数表示将服务器端套接字绑定到指定的端口号上
5,
connectionSocket, addr = serverSocket.accept() # 接收到客户连接请求后,建立新的TCP连接套接字
connectionSocket
:这是一个新创建的已连接套接字对象,它用于实际的数据传输。通过这个套接字,服务器可以与特定的客户端进行通信。服务器可以使用connectionSocket
来发送和接收数据,以满足客户端的请求或提供服务。
addr
:这是客户端的地址信息,包含了客户端的 IP 地址和端口号。通常,它是一个元组,例如(clientIP, clientPort)
。通过获取客户端地址,服务器可以知道是哪个客户端发起了连接请求,可以根据需要记录日志、进行身份验证等操作
6,
print('Accept new connection from %s:%s...' % addr)
%
是字符串格式化操作符,用于将变量的值插入到字符串中的占位符位置。
'Accept new connection from %s:%s...'
是要打印的字符串,其中%s
是占位符。
% addr
是一个元组,在字符串中的%s
占位符位置被addr
中的值替换。addr
是客户端的地址信息,通常是一个包含客户端 IP 地址和端口号的元组。打印结果将显示为类似于
Accept new connection from 192.168.0.1:5000...
的形式,其中192.168.0.1
是客户端的 IP 地址,5000
是客户端的端口号
7,
sentence = connectionSocket.recv(1024) # 获取客户发送的字符串
connectionSocket
是一个已连接的套接字对象,它用于与特定客户端进行通信。
recv(1024)
是一个阻塞调用,它会从已连接套接字中接收最多 1024 字节的数据。如果客户端发送的数据量超过 1024 字节,则可能需要多次调用recv()
来完整接收所有数据。
setence
是一个变量,用于存储接收到的客户端数据(字符串)。该行代码将等待,直到客户端发送数据或者客户端关闭连接。一旦有数据到达,它将被接收并存储在
setence
变量中
8,Client有connect(),而Server没有connect()的解释👇
TCP Server:
- TCP 服务器通常处于被动等待状态,它通过监听指定的端口,等待客户端的连接请求。
- 一旦有客户端发送连接请求,服务器会接受该连接并创建一个新的套接字,即已连接套接字(Connected Socket)。
- 已连接套接字与特定客户端建立了一对一的通信管道,可以进行双向的数据传输。
- 在服务器端,已连接套接字用于与客户端进行通信,无需显式地调用
connect
方法。TCP Client:
- TCP 客户端主动发起连接请求,它需要知道服务器的 IP 地址和端口号。
- 客户端使用
connect
方法来连接到服务器指定的 IP 地址和端口。connect
方法会在客户端和服务器之间建立一条连接,并返回一个已连接套接字。- 客户端通过已连接套接字与服务器进行通信,发送请求并接收响应
🎂客户端
1,
sentence = input('Input lowercase sentence:').encode() # 用户输入信息,并编码为bytes以便发送
input('Input lowercase sentence:')
:这是一个输入函数,它会在控制台中显示提示信息"Input lowercase sentence:"并等待用户输入。用户可以输入一个小写句子。用户输入后,
input
函数会返回用户输入的内容作为字符串。
.encode()
:对返回的字符串进行编码操作。在这里,使用默认的UTF-8编码将字符串转换为字节序列。编码后的字节序列可以被网络传输或存储。最终,编码后的字节序列被赋值给变量
sentence
。现在,sentence
变量中存储的是用户输入的小写句子经过编码后的字节表示形式
2,将接收到的字节序列转换为字符串并在控制台中显示
print(modifiedSentence[0].decode()) # 显示信息
modifiedSentence[0]
:这是一个从modifiedSentence
字节序列中获取第一个元素的操作。在网络通信中,通常会将接收到的数据作为字节序列进行处理,因此需要使用字节序列的索引来访问其中的元素。
.decode()
:对字节序列进行解码操作。在这里,使用默认的UTF-8编码将字节序列转换为字符串。解码后的字符串可以在控制台中显示
🔥UDP套接字(实验2)
UDP连接比TCP少了个 监听 listen(),函数从 sent() 变成了 sendto(),建立的套接字是UDP的
🎂服务器
from socket import *
serverPort = 12000 # 自己指定的端口
serverSocket = socket(AF_INET, SOCK_DGRAM) # 使用IPv4协议,创建UDP套接字
serverSocket.bind(('', serverPort)) # 套接字绑定到指定端口
print('The server is ready to receive')
while True:
message, clientAddress = serverSocket.recvfrom(2048) # 接受客户端信息,获取客户端地址
modifiedMessage = message.upper() # 客户端发来的字符串变大写
serverSocket.sendto(modifiedMessage, clientAddress) # 通过客户端地址,将字符串发送回客户端
🎂客户端
from socket import *
serverName = '192.168.15.1' # IPv4 的服务器地址
serverPort = 12000 # 人为指定的端口号
clientSocket = socket(AF_INET, SOCK_DGRAM) # 使用IPv4协议,创建UDP套接字
message = input('Input lowercase sentence:').encode() # 用户输入,并编码为bytes以便发送
clientSocket.sendto(message, (serverName, serverPort)) # 套接字发送到服务器
modifiedMessage, serverAddress = clientSocket.recvfrom(2048) # 从服务器接受字符串和地址
print(modifiedMessage.decode()) # 显示服务器返回的信息
clientSocket.close()
效果和过程,与TCP类似