计算机网络 —— 网络编程(套接字深度理解)
计算机网络 —— 网络编程(套接字深度理解)
- 套接字对于服务端和用户端的意义
- 服务端(Server)
- 1. 监听连接
- 2. 接收数据
- 3. 发送响应
- 客户端(Client)
- 1. 发起连接
- 2. 发送请求
- 服务端的套接字
- `0.0.0.0:8888` 在服务端的意义
- 与 `192.27.56.33:8888` 的区别
- 总结
- 客户端的套接字
- 在客户端的意义
- 1. 绑定本地地址为 `0.0.0.0`
- 2. 绑定本地端口
- 区别于服务端
- 具体IP地址 vs. `0.0.0.0`
- 客户端的实际用法
- 总结
- 验证跨主机通信
之前我们有了解过UDP编程中的接口,对套接字有了一定的了解,这些接口一开始用的时候会有点懵,所以这次我们来梳理一下这些接口和套接字的理解。
套接字对于服务端和用户端的意义
套接字(Socket)是网络编程中的一个抽象概念,它为应用程序提供了通信的能力。对于服务端和客户端来说,套接字的意义有所不同,但它们共同构成了实现客户端-服务器模型的基础。以下是套接字在服务端和客户端的具体意义:
服务端(Server)
1. 监听连接
- 作用:服务端使用套接字来监听来自客户端的连接请求。这意味着服务端绑定到一个特定的IP地址和端口,并等待客户端发起连接。
- 示例代码:
sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用接口 bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); listen(serverSocket, BACKLOG); // 开始监听连接
2. 接收数据
- 作用:服务端通过套接字接收客户端发送的数据。它可以处理多个客户端的并发请求,每个连接通常对应一个独立的套接字。
- 示例代码:
while (true) { int clientSocket = accept(serverSocket, nullptr, nullptr); // 处理客户端连接 // ... close(clientSocket); }
3. 发送响应
- 作用:服务端可以通过套接字向客户端发送响应或数据。这可以是对客户端请求的回复,也可以是主动推送的信息。
- 示例代码:
send(clientSocket, response, strlen(response), 0);
客户端(Client)
1. 发起连接
- 作用:客户端使用套接字来发起与服务端的连接请求。它指定要连接的服务端的IP地址和端口。
- 示例代码:
sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); serverAddr.sin_addr.s_addr = inet_addr("SERVER_IP"); connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
2. 发送请求
-
作用:客户端通过套接字向服务端发送请求或数据。这些数据可以是命令、文件或其他形式的消息。
-
示例代码:
send(clientSocket, request, strlen(request), 0);
-
服务端:套接字用于监听客户端连接、接收数据、发送响应,并可能同时处理多个客户端的请求。
-
客户端:套接字用于发起与服务端的连接、发送请求、接收响应,并根据需要进行后续交互。
-
共同点:套接字提供了一个统一的接口,使得服务端和客户端能够通过网络进行高效、可靠的通信。
服务端的套接字
0.0.0.0:8888
在服务端的意义
- 监听所有网络接口:当服务器绑定到
0.0.0.0:8888
时,它表示该服务器将监听所有可用的IPv4网络接口上的端口8888
。这意味着无论客户端通过哪个IP地址(例如,服务器的私有IP、公网IP或环回地址)连接到服务器,只要目标端口是8888
,服务器都将接受这些连接。
- 灵活性更高:使用
0.0.0.0
提供了更大的灵活性,因为服务器不需要事先知道它将从哪个IP地址接收数据。这对于多宿主主机(具有多个网络接口和多个IP地址的服务器)特别有用,因为它允许服务器同时处理来自不同网络的数据。
- 安全性考虑:由于
0.0.0.0
监听所有接口,因此可能带来安全风险,特别是如果服务器暴露在公共互联网上。通常建议结合防火墙规则或其他安全措施来限制访问。
与 192.27.56.33:8888
的区别
- 特定IP地址 vs. 所有IP地址:
192.27.56.33:8888
表示服务器仅监听特定IP地址192.27.56.33
上的端口8888
。这意味着只有发送到这个特定IP地址的数据包会被服务器接收并处理。0.0.0.0:8888
则表示服务器监听所有可用网络接口上的端口8888
,包括私有IP、公网IP和环回地址等。
- 应用场景:
- 如果您确切知道服务器应该只接收来自某个特定网络接口的数据,那么绑定到具体的IP地址(如
192.27.56.33
)是有意义的,并且可以提高安全性。- 如果您希望服务器能够处理来自任何网络接口的数据,或者不确定具体会使用哪个IP地址,则可以使用
0.0.0.0
来提供更大的灵活性。
- 配置复杂性:
- 绑定到特定IP地址可能需要更复杂的配置,尤其是在多宿主环境中。您可能需要为每个网络接口分别设置规则。
- 使用
0.0.0.0
简化了配置,但同时也意味着您需要额外的安全措施来确保不会无意中开放不必要的访问。
总结
0.0.0.0:8888
:表示服务器监听所有可用的IPv4网络接口上的端口8888
,适用于需要最大灵活性的情况,但也需要注意安全问题。192.27.56.33:8888
:表示服务器仅监听特定IP地址192.27.56.33
上的端口8888
,适用于需要精确控制通信路径和提高安全性的场景。
客户端的套接字
在客户端,0.0.0.0:8888
的使用方式和意义与服务端有所不同。实际上,在客户端绑定 0.0.0.0
并不常见,因为客户端通常不需要指定一个通配符地址来发送数据。让我们详细解释一下它的含义以及它与具体IP地址(如 192.27.56.33
)的区别。
在客户端的意义
1. 绑定本地地址为 0.0.0.0
- 默认行为:大多数情况下,客户端不需要显式绑定本地地址。如果客户端没有指定本地IP地址和端口,操作系统会自动选择一个可用的本地端口,并根据路由表选择最合适的网络接口。
- 绑定
0.0.0.0
的特殊性:在客户端中绑定0.0.0.0
实际上是没有意义的,因为0.0.0.0
表示所有可用的网络接口,而客户端通常只需要从一个特定的本地地址发送数据。此外,客户端通常不会绑定到一个固定的本地端口,而是让操作系统分配一个临时端口。
2. 绑定本地端口
- 固定本地端口:客户端可以选择绑定到一个特定的本地端口,例如
8888
。这可以用于某些需要固定源端口的应用场景,比如防火墙规则、NAT穿透等。
- 选择本地IP地址:客户端也可以选择绑定到一个特定的本地IP地址,以确保数据包从特定的网络接口发送出去。这对于多宿主主机(具有多个网络接口和多个IP地址的设备)特别有用。
区别于服务端
- 服务端:
0.0.0.0:8888
表示服务器监听所有可用网络接口上的端口8888
,适用于接收来自任何网络的数据。- 客户端:
0.0.0.0:8888
在客户端中没有实际意义,因为客户端不需要监听所有接口,而是主动连接到远程服务器。客户端通常只绑定到特定的本地端口或让操作系统自动选择。
具体IP地址 vs. 0.0.0.0
- 特定IP地址(如
192.27.56.33
):- 绑定本地地址:客户端可以选择绑定到一个特定的本地IP地址,以确保数据包从特定的网络接口发送出去。这对于多宿主主机特别有用。
- 绑定本地端口:客户端可以选择绑定到一个特定的本地端口,以确保数据包带有固定的源端口。
0.0.0.0
:- 无实际意义:在客户端中,绑定
0.0.0.0
没有实际意义,因为客户端不需要监听所有接口,而是主动连接到远程服务器。- 默认行为:如果不绑定本地地址,操作系统会自动选择最合适的网络接口和可用的本地端口。
客户端的实际用法
- 不绑定本地地址:大多数情况下,客户端不需要显式绑定本地地址和端口。操作系统会自动处理这些细节。
- 绑定本地端口:在某些情况下,客户端可能需要绑定到一个特定的本地端口。例如,为了通过防火墙规则或NAT配置进行通信。
- 绑定本地IP地址:对于多宿主主机,客户端可以选择绑定到特定的本地IP地址,以确保数据包从特定的网络接口发送出去。
总结
- 服务端:
0.0.0.0:8888
表示服务器监听所有可用网络接口上的端口8888
,适用于接收来自任何网络的数据。- 客户端:
0.0.0.0:8888
在客户端中没有实际意义,因为客户端不需要监听所有接口,而是主动连接到远程服务器。客户端通常只绑定到特定的本地端口或让操作系统自动选择。
这里区分一下,这里客户端不用显示绑定IP地址,但是如果我们想要发送信息到服务端时还是要知道服务端的IP和端口,这个是两个问题,要区别清楚。
验证跨主机通信
既然我们学习网络通信的接口,证明我们可以进行跨主机的通信,我们可以利用Windows和Linux平台验证一下,我们将Linux作为服务器,将Windows作为客户端:
我们首先在Linux端编写服务器的逻辑:
#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <cstring>
#include <unistd.h>
#include <cstdint>
// 定义默认端口号
const static uint16_t defaultport = 8888;
class UdpServer
{
public:
// 构造函数,允许指定端口号,默认使用 `defaultport`
UdpServer(const uint16_t port = defaultport)
: _port(port)
{
}
// 初始化服务器
void Init()
{
// 创建一个UDP套接字
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
std::cout << "Create socket fail!" << std::endl;
exit(1); // 如果创建失败,退出程序
}
std::cout << "Create socket successfully" << std::endl;
// 初始化服务器地址结构
struct sockaddr_in server;
server.sin_family = AF_INET; // 使用IPv4协议族
server.sin_port = htons(_port); // 设置端口,并将其转换为网络字节序
server.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用网络接口
// 将套接字绑定到指定的IP和端口
int n = bind(_socketfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cout << "Bind fail!" << std::endl;
exit(1); // 如果绑定失败,退出程序
}
std::cout << "Bind successfully!" << std::endl;
}
// 启动服务并开始接收数据
void Start()
{
char buffer[1024]; // 缓冲区用于存储接收到的数据
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收来自客户端的数据报
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = '\0'; // 确保字符串以null字符结尾
std::cout << "Client says# " << buffer << std::endl; // 输出接收到的消息
}
}
}
// 析构函数(当前未实现任何清理操作)
~UdpServer()
{
}
private:
uint16_t _port; // 服务器监听的端口号
int _socketfd = -1; // 套接字描述符,-1表示尚未创建
};
#include"UdpServer.hpp"
#include<memory>
void Usage()
{
std::cout << "Usage: ./UdpServer port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage();
return 1;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr usr = std::make_unique<UdpServer>(port);
usr->Init();
usr->Start();
return 0;
}
我们在windows上可以用VS来编写这样的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>
#pragma warning(disable:4996)
#pragma comment(lib, "Ws2_32.lib")
int main() {
// 初始化 Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
std::cerr << "WSAStartup failed: " << iResult << std::endl;
return 1;
}
// 创建套接字
SOCKET SendSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (SendSocket == INVALID_SOCKET)
{
std::cerr << "Error at socket(): " << WSAGetLastError() << std::endl;
WSACleanup();
return 1;
}
// 设置服务器地址和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 端口号
serverAddr.sin_addr.s_addr = inet_addr("43.138.14.12"); // 服务器地址
// 发送数据
while (true)
{
// const char* message = "Hello, UDP Server!";
char message[1024];
std::cout << "Enter message to send: ";
std::cin.getline(message, sizeof(message));
int sendResult = sendto(SendSocket, message, strlen(message), 0,
(struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (sendResult == SOCKET_ERROR) {
std::cerr << "sendto failed: " << WSAGetLastError() << std::endl;
closesocket(SendSocket);
WSACleanup();
return 1;
}
else {
std::cout << "Sent a datagram to the server." << std::endl;
}
}
// 清理
closesocket(SendSocket);
WSACleanup();
return 0;
}
然后分别编译运行,我们可以看到Windows上的信息可以传到Linux上去了:
如果8888的端口号不行,就换换其他的端口,应该没问题。