-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
C++ Socket套接字编程 Socket Programming In C++ (Windows)[进行中] #221
Comments
1. 介绍1.1 介绍课程介绍:C 语言与 Socket 编程大家好,我是 Chaka,欢迎大家来到我的课程《C 语言与 Socket 编程》。让我先简单介绍一下我自己。我是一名软件专业人员,拥有 16 年的工作经验。我的主要工作领域包括 C++ 开发、Socket 编程和 Windows 基础知识。 我毕业于印度北方邦戈尔哈特技术大学,获得了计算机科学与技术专业的学位(2006 年)。此外,我还拥有数学学士学位(2003 年)。 课程目标本课程面向初学者和中级学习者,内容将涵盖 Socket 编程 的基础知识,并帮助大家在掌握这些基础后,能够进一步提升到中级水平。只要您理解了课程中的概念,您就能够在此基础上进一步学习和发展。 在本课程中,我们将学习到以下内容: 1. Socket 编程基础
2. 网络字节顺序
3. 使用 TCP 的 Socket 编程
4. 使用 UDP 的 Socket 编程
通过本课程的学习,您将获得必要的 Socket 编程技能,并能够在实际应用中开发网络程序。 祝大家学习愉快,感谢您的参与! |
2. 套接字介绍2.1 什么是套接字Socket 编程基础与概念让我们从 Socket 编程的基本概念 开始,首先了解什么是 Socket 编程? 什么是 Socket 编程?Socket 编程 是一种连接两个程序的方式,使得它们能够相互通信、共享数据以及发送和接收消息。换句话说,如果你希望两个程序能够交换数据,那么就需要使用 Socket 编程。 在 Socket 编程 中,有两个重要的角色:
服务器是一个“监听者”,它会不断地运行,并等待来自其他程序的请求。而客户端则通过连接到服务器来访问它提供的服务或资源。 服务器和客户端之间的通信有时,服务器不仅仅是与一个客户端通信,它还可能在客户端之间充当中介,允许多个客户端之间的通信。例如,在聊天服务器的场景中,当你与朋友聊天时,并不是直接通过网络连接与朋友对话。事实上,聊天服务器在你和朋友之间充当了中介,促进了你们之间的通信。 因此,无论是服务器程序还是客户端程序,它们都是 Socket 程序。服务器程序的作用是让多个客户端能够相互通信,而客户端则是通过连接服务器来获取所需服务或资源。 Socket 是什么?在 编程 中,Socket 类似于 文件描述符。就像你打开一个文件来读取或写入时,操作系统会为你创建一个流通道,用于读取或写入该文件。类似地,当两个程序需要通信时,它们会打开一个 通信通道,并且通过 Socket 描述符 来进行通信。 通过这个 Socket 描述符,两个程序就能够相互通信。Socket 描述符本质上是通信通道的唯一标识符,它使得程序能够通过指定的通信路径与其他程序交换数据。 Socket 的类型在网络编程中,常见的有两种类型的 Socket:
在接下来的讲解中,我们将详细探讨这两种类型的 Socket,并了解它们的不同特性和应用场景。 结束语感谢您的关注!在后续的课程中,我们将深入探讨 Socket 编程的各个方面,包括如何在实际应用中使用这些技术来进行网络通信。 2.2 面向连接和无连接的套接字Stream Sockets 与 Datagram Sockets 的区别Stream Sockets(流式套接字)首先,让我们来了解 流式套接字。流式套接字是 面向连接的套接字,这意味着每当通信开始时,必须保持一个活动连接。如果连接丢失,通信将无法继续。因此,流式套接字的关键特性包括:
Datagram Sockets(数据报套接字)接下来,我们来看 数据报套接字。数据报套接字是 无连接的套接字,它不需要持续的连接来进行通信。每当数据包发送时,它会作为一个独立的包发送,而不需要先建立一个连接。它的关键特点包括:
总结
通过以上比较,你可以看到两者的主要区别在于 连接的有无 和 数据的可靠性。流式套接字通过 TCP 提供可靠的通信,而数据报套接字则依赖于 UDP 提供快速但不可靠的通信。 2.3 IP 地址(版本 4 和版本 6)IPV4 和 IPV6什么是 IPV4?首先,让我们了解一下 IPV4。IP 是 Internet Protocol(互联网协议) 的缩写,IP 地址用于在网络中路由数据包。IPV4 是一种 网络路由协议,它使用由四个字节(32 位)组成的地址。例如,常见的 IP 地址格式像 随着计算机、笔记本、手机以及其他设备数量的增加,IP 地址需求急剧增加。由于需求的增长,IPv4 地址(由四个字节组成)已经无法满足需求了。这个问题由 Vint Cerf 提出了。Vint Cerf 是一位美国网络先锋,被称为“互联网之父”,他在 1943 年出生,他预见到很快我们就会耗尽 IPv4 地址。 为什么需要 IPV6?由于 IPV4 的地址空间已经接近枯竭,IPv6 应运而生。IPv6 使用 128 位 来生成 IP 地址,而 IPV4 仅使用 32 位。IPv6 地址的可用地址空间是 IPv4 的极大扩展,它能够生成约 340万万亿万亿个地址,这一数量足够满足全球范围内设备的需求。 IPV6 地址的格式IPv6 地址通常采用 十六进制 的形式,地址之间用冒号(
这两者代表的是同一个地址。 循环地址(Loopback Address)IPv6 也有 回环地址,类似于 IPv4 中的 总结
如果你对 IPV6 与 IPV4 之间的差异、优势等有更深入的兴趣,建议你进一步查阅相关的互联网资源,如 JavaPoint 等网站。 2.4 阻塞与非阻塞套接字阻塞与非阻塞套接字什么是阻塞套接字?在默认情况下,任何通过套接字 API 调用打开的套接字都是 阻塞套接字。这意味着,当你进行 输入输出操作,或者说进行数据发送和接收时,程序会被阻塞,直到操作完成。举个例子,如果你正在向服务器或客户端发送消息,程序将一直阻塞,直到消息成功传输并确认完成。 如果你作为 客户端,尝试连接到服务器,那么在连接建立之前,客户端线程也会被阻塞,直到服务器响应。这意味着在 阻塞模式 下,操作必须等到完成才能继续,程序无法执行其他任务,必须等待连接或数据传输完成。 非阻塞套接字与阻塞套接字不同,非阻塞套接字允许程序在进行套接字操作时,不会停下来等待。以 浏览器 为例,当你打开浏览器并尝试连接到 如果你不希望在进行发送、接收或连接时让程序阻塞,应该使用非阻塞套接字。这样,如果连接没有立即建立,程序不会停下来等待,反而可以继续执行其他任务。 如何实现非阻塞套接字?要将套接字设置为非阻塞,你需要使用特定的 API 或 系统调用。这些方法可以使你的套接字不再阻塞,允许它在等待操作完成时执行其他任务。具体的实现方法会在后续的视频或实践中进一步讲解。 总结
理解了这两种套接字的区别后,你可以根据实际需要选择使用阻塞还是非阻塞套接字。 2.5 理解网络字节顺序网络字节顺序(Network Byte Order)印度序(Indianness)在理解 网络字节顺序 之前,我们首先需要了解 印度序(Indianness)的概念。印度序指的是在计算机中如何存储多字节数据的顺序。根据字节的存储顺序,通常有两种格式:大端格式(Big Endian) 和 小端格式(Little Endian)。
大端格式与小端格式的示例假设我们有一个整数 15,它的二进制表示为 在 小端格式 中,最不重要的字节( 例如:
网络字节顺序在 网络字节顺序 中,使用的是 大端格式(Big Endian)。这意味着在通过网络传输数据时,数据会按照 大端格式 存储和传输,无论发送端和接收端的机器是采用什么样的字节顺序。
字节顺序转换函数为了实现不同字节顺序之间的转换,套接字 API 提供了一些转换函数。例如:
重要提示
小结
2.6 开始前需要了解的重要结构重要的结构体和头文件在开始编写套接字编程代码之前,了解一些关键的结构体是非常重要的。这些结构体将在您创建和配置套接字时起到关键作用。以下是几个在套接字编程中非常重要的结构体,您需要了解它们的用途和如何使用它们。 1. Socket 结构体首先,您需要了解 Socket 结构体,它是与网络连接相关的核心结构之一。在 Windows 环境中,您可以在 MSDN 在线库中找到有关该结构体的详细信息。您可以访问 MSDN 了解更多内容。这个结构体很简单,且已经被详细解释,适合初学者理解。 Socket 结构体是套接字编程中最基本的结构之一,它定义了您如何与网络交互。您可以使用此结构来设置套接字、连接到远程服务器、监听特定端口等。 2. Socket_in 结构体另一个重要的结构体是 Socket_in 结构体,它通常用于描述地址信息。特别是在 IPV4 环境中,您将使用此结构体来绑定套接字地址、连接到服务器或监听来自客户端的请求。
3. Socket_in6 结构体随着 IPV6 的引入,套接字相关的结构体也有所变化。对于 IPV6 网络,您需要使用 Socket_in6 结构体,它与 Socket_in 结构体非常相似,但是支持更大的地址范围(128 位)。因此,在使用 IPV6 网络时,您必须了解并使用 Socket_in6 结构体。
4. Addrinfo 结构体另一个重要的结构体是 Addrinfo。它是套接字编程中用于存储与主机相关的网络信息的结构体,通常用于获取有关主机的详细信息。例如,它可以包含有关主机的 IP 地址、端口号等信息。这个结构体是由 getaddrinfo 函数使用的。
结构体总结
学习资源这些结构体的具体定义和用法可以在 MSDN 在线文档中找到,建议您阅读这些资源,以便理解如何使用这些结构体进行套接字编程。 下一步
感谢您的阅读,接下来的部分将涉及到更多关于实际操作的讲解。 |
3. 设置 Windows 开发环境3.1 Windows 上的套接字编程Windows环境中的套接字编程设置在本讲中,我们将讨论如何在Windows环境中设置套接字编程开发环境。首先,我们需要做出一些决定,特别是选择使用的集成开发环境(IDE)。接下来,我们将了解与套接字编程相关的一些头文件和库文件。 1. 选择IDE在这门课程中,我将使用 Visual Studio 2019 作为集成开发环境。如果您喜欢使用其他IDE,也完全可以,但请确保您熟悉所选IDE的使用方法。因为我整个课程中不会使用其他IDE,所以建议您确保能够熟练操作 Visual Studio 2019。 2. 套接字编程所需的头文件对于Windows平台上的套接字编程,有两个主要的头文件:
这两个头文件都用于套接字编程,但在任何时刻您只能使用其中一个。您不能同时使用这两个头文件。winsock2.h 是较新的版本,它包含了 winsock.h 中的所有函数,因此我们推荐使用 winsock2.h。 兼容性说明如果您之前使用 winsock.h 写过程序,您可以轻松地将程序移植到使用 winsock2.h 的代码中。也就是说,winsock2.h 是向后兼容的。假设您在2010年或2011年用 winsock.h 写了某个程序,那么只需要将头文件改为 winsock2.h,代码就能继续编译并正常运行。这是因为 winsock2.h 包含了 winsock.h 中的所有内容,并且提供了更多的API和功能。 3. 套接字编程所需的库文件接下来,我们需要了解套接字编程所使用的库文件。Windows中有两个主要的库文件:
4. DLL文件的作用除了静态库(lib文件),这些库还有对应的 DLL文件,它们在运行时提供函数的实现。具体来说:
这意味着,您编写的程序在运行时依赖这些DLL文件来提供函数的实际实现。 5. 设置环境在安装 Visual Studio 2019 后,相关的 DLL文件(如 wsock32.dll 和 ws2_32.dll)会被自动放置在正确的位置,通常是在 System32 文件夹或 Path 环境变量中。这意味着,在使用 Visual Studio 2019 时,您无需手动配置这些文件的位置,它们已经准备好了。 6. 总结在Windows上进行套接字编程时,您需要了解以下几个关键点:
在接下来的课程中,我们将演示如何在 Visual Studio 2019 中实际操作并进行套接字编程。 感谢您的阅读,敬请期待下一节内容! 3.2 为套接字编程配置 Windows 操作系统在Visual Studio 2019中设置套接字编程环境欢迎回来,在本视频中,我们将展示如何在 Visual Studio 2019 中为套接字编程设置开发环境。接下来,我将为您演示如何创建一个新项目并配置所需的头文件和库文件。 1. 创建新的C++控制台应用程序项目首先,打开 Visual Studio 2019,并创建一个新的C++控制台应用程序项目。选择一个您希望存储项目的位置,并为项目命名为 UtilitySockets。点击创建按钮后,Visual Studio会自动生成一些代码文件。您应该熟悉这个过程,因为在使用 Visual Studio 时,IDE 会自动为您生成一些默认的代码。 2. 选择并添加适当的头文件我们接下来要讨论的是在进行套接字编程时需要使用的头文件。在Windows环境下,您可以使用以下两种头文件:
如何使用这些头文件如果您选择使用 winsock.h,请确保在代码中引用该头文件并编译代码: #include <winsock.h>
using namespace std; 如果您使用 winsock.h,编译是成功的。但如果您尝试同时使用两个头文件 winsock.h 和 winsock2.h,编译会失败。原因在于这两个头文件中包含了一些相同的符号,因此它们不能在同一源文件中同时存在。 如何选择头文件
3. 配置库文件我们还需要为套接字编程配置适当的库文件。具体的库文件如下:
如何配置库文件在使用 winsock.h 时,您需要引用 wsock32.lib,并且确保在项目设置中链接到该库文件。 如果您使用的是 winsock2.h,则需要引用 ws2_32.lib,并在项目设置中进行链接。 如何配置DLL文件
这些 DLL文件 在您安装 Visual Studio 2019 时会自动放置在系统的正确位置(例如,System32 文件夹或 Path 环境变量中)。因此,您无需手动配置这些 DLL 文件。 4. 在项目设置中配置库文件如果您不想在代码中明确指定库文件,也可以通过 Visual Studio 的项目设置来配置库文件的引用:
您还可以直接在代码中使用以下指令来指示链接器: #pragma comment(lib, "ws2_32.lib") 如果您使用的是 winsock2.h,可以使用上面的代码来告诉编译器链接 ws2_32.lib。 5. 代码编译与运行一旦您配置好头文件和库文件,点击编译,您的程序将成功编译并准备好进行套接字编程。如果您遇到任何错误,请确保您引用了正确的头文件和库文件,并且确保项目中所有设置都正确。 6. 下一步:加载套接字DLL在下一个视频中,我们将讨论如何加载套接字DLL文件。在Windows中,套接字API的使用不同于UNIX系统。与UNIX不同,Windows中的套接字编程需要在使用API之前先加载相关的DLL文件。我们将介绍如何执行这些操作,并确保套接字函数能够正确工作。 感谢您的观看,期待在下一个视频中与您继续讨论套接字编程的实践! 3.3 测试系统准备情况的示例程序在Windows环境中使用套接字API的准备工作欢迎回来!在本视频中,我们将讨论在开始使用套接字API之前需要做哪些准备工作。特别是,在Windows环境中,您需要调用一个特定的函数来加载套接字所需的库。 1. 使用
|
4. 套接字编程实践(TCP) - 单客户端套接字服务器4.1 打开一个套接字如何在套接字编程中打开套接字以开始通信欢迎回来!在本视频中,我们将讨论如何在服务端和客户端程序中打开套接字以开始通信。无论您是在编写服务器端程序,还是客户端程序,首先都需要在调用 目前我们将展示如何在服务器端打开套接字。 1. 创建并初始化套接字首先,您需要创建一个变量来表示套接字。在服务器端,我们通常称它为 SOCKET listenerSocket; 在此示例中, 2. 打开套接字接下来,我们通过调用
将这些值传递给 listenerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 3. 错误处理当调用 if (listenerSocket == INVALID_SOCKET) {
std::cout << "Socket failed to open!" << std::endl;
} else {
std::cout << "Socket opened successfully!" << std::endl;
} 4. 进一步调试为了验证套接字是否成功打开,您可以设置一个断点,在 Socket opened successfully! 如果调用失败,则会输出: Socket failed to open! 5. 套接字的作用套接字可以看作是一个文件描述符,类似于打开文件时获得的文件描述符。套接字描述符允许程序通过网络与其他程序进行通信。当您成功打开套接字时,您可以使用该描述符进行读写操作,就像与文件进行操作一样。 例如,您可以使用套接字描述符来接收和发送数据,建立连接等。 6. Windows与Linux的区别需要注意的是,在 Windows 系统中,您必须先调用 7. 代码示例以下是完整的示例代码,用于打开一个监听套接字: #include <iostream>
#include <winsock2.h> // Windows平台需要此头文件
SOCKET listenerSocket;
int main() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化Winsock
if (result != 0) {
std::cout << "WSAStartup failed: " << result << std::endl;
return 1;
}
listenerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字
if (listenerSocket == INVALID_SOCKET) {
std::cout << "Socket failed to open!" << std::endl;
} else {
std::cout << "Socket opened successfully!" << std::endl;
}
// 其他代码,例如绑定、监听等
return 0;
} 8. 结论在Windows环境中进行套接字编程时,打开套接字是一个必不可少的步骤。使用 4.2 设置套接字选项套接字选项设置函数
|
5. 套接字编程实践 - 多客户端处理5.1 打开一个新的套接字作为监听欢迎回来。在这一部分中,我们将学习如何创建一个可以同时处理多个客户端的服务器。我要展示的程序是我之前为单个客户端处理的服务器程序的相似版本,但是我们将对这个程序进行一些修改,使其成为一个可以同时处理多个客户端的服务器。 程序的基本架构首先,这个程序和我们之前编写的单客户端服务器程序是相似的。唯一的不同之处是,我们将在这个程序中做一些更改,使其能够处理多个客户端的连接。
多客户端服务器处理的关键修改到这里为止,代码和单客户端处理的服务器代码是相同的。但从这一部分开始,我们需要做出修改,以支持同时处理多个客户端的连接。
下一步接下来的内容将在视频中进一步讨论如何修改代码来处理多个客户端请求。这将涉及如何管理并发的客户端连接,可能会使用线程池或多进程来同时处理多个客户端的连接请求。 总结在这部分,我们回顾了多客户端服务器的基本架构,并介绍了需要更改的地方。接下来的部分将深入讨论如何实现多线程或异步机制来真正实现一个可以同时处理多个客户端的服务器。 希望你能理解这个程序的工作原理,并准备好下一步的多客户端处理。我们将在下节课继续讲解如何实现这一目标。 感谢观看,下次见! 5.2 介绍 FD_SET 和准备 select 列表欢迎回来。在这一讲中,我们将讨论如何准备监听电路,使其能够准备好接受新的连接。同时,我们还将理解文件描述符集(FD Set)的概念。 文件描述符集(FD Set)的声明在继续讲解文件描述符集(FD Set)之前,我们首先需要声明一个文件描述符集。可以通过类似以下的方式来声明: fd_set fdr; 这个 文件描述符集的工作原理
为此,我们维护一个变量 例如: int max_fd = listener_socket; 通过 客户端套接字的管理假设我们最多支持5个客户端连接,那么我们需要声明一个客户端套接字数组: int client_sockets[5] = {0}; 这里, 向文件描述符集中添加套接字现在,我们需要将监听套接字以及已连接的客户端套接字添加到文件描述符集中。在处理过程中,如果有新的客户端连接,我们将其对应的套接字也加入到文件描述符集中,以便 首先,我们将监听套接字添加到 FD_ZERO(&fdr); // 清空文件描述符集
FD_SET(listener_socket, &fdr); // 将监听套接字添加到集 接下来,我们检查客户端套接字数组。如果某个客户端套接字不为0,则将其添加到 for (int i = 0; i < 5; i++) {
if (client_sockets[i] != 0) {
FD_SET(client_sockets[i], &fdr);
}
} 运行中的服务器服务器将持续运行,且需要不断检查是否有新的客户端连接或现有客户端是否发送了数据。为了实现这一点,我们将上述的代码放入一个无限循环中,这样服务器就能持续检查所有的套接字状态。 while (true) {
// 设置文件描述符集并调用select
FD_ZERO(&fdr);
FD_SET(listener_socket, &fdr);
// 将已连接的客户端套接字添加到文件描述符集中
for (int i = 0; i < 5; i++) {
if (client_sockets[i] != 0) {
FD_SET(client_sockets[i], &fdr);
}
}
// 调用select函数来检测哪个套接字准备好
int activity = select(max_fd + 1, &fdr, NULL, NULL, NULL);
// 处理activity返回的结果
// 如果监听套接字有活动,接受新的连接
// 如果客户端套接字有活动,处理接收到的数据
} 编译与测试此时,代码实际上只是设置了文件描述符集,并将监听套接字和客户端套接字加入到集合中。到目前为止,我们并没有处理实际的客户端数据接收或新连接的接受。 我们将在下一节讲解 总结在这一讲中,我们讨论了如何准备文件描述符集来监听客户端连接和数据。我们通过创建文件描述符集,并将监听套接字和客户端套接字添加到集合中来实现这一点。接下来,我们将在下一讲中深入研究 感谢观看,下次见! 5.3 select 系统调用等待新客户端欢迎回来!在本讲中,我们将学习如何使用 1. 复习在之前的讨论中,我们只有一个套接字,即监听套接字,因此将 2. 使用 Select API在使用 struct timeval tv;
tv.tv_sec = 1; // 设置超时为 1 秒
tv.tv_usec = 0; // 微秒部分为 0 然后,我们就可以使用 int err = select(max_fd + 1, &rfds, NULL, NULL, &tv); 其中:
3. 处理 Select 调用的返回值
if (err > 0) {
// 处理新连接或处理已连接客户端的消息
} else if (err == 0) {
// 没有新的连接或消息
printf("No new connection or message\n");
} else {
// 出现错误,退出程序
printf("Select failed, closing server\n");
exit(EXIT_FAILURE);
} 4. 为什么要每次重新设置文件描述符集合
例如,如果我们仅在循环外部设置一次文件描述符集合: // 错误的做法,文件描述符集合只设置一次 那么,在 5. 演示在代码中,我们会看到,如果我们每次不设置文件描述符集合,就会遇到错误: // 错误的代码:未每次设置文件描述符集合 然后运行程序时,第一次返回 0,表示没有新连接或消息。但是第二次运行时,返回值将是 -1,因为文件描述符集合已经被 正确的做法是每次调用 // 正确的做法:每次调用前设置文件描述符集合 这样程序就会正确运行,等待新连接或消息的到来。 6. 总结在这节课中,我们学习了如何使用 7. 下节课预告下一节课我们将深入探讨如何处理新连接以及从现有客户端接收消息。到那时,您将能够实现完整的服务器程序,可以接受新连接并与客户端交换数据。 5.4 接受新客户端连接本视频概述在本视频中,我们将了解如何在有新客户端尝试连接时,接受这个连接。 前置知识在前一个视频中,我们理解了如何使用 监听套接字和检测新连接你可能已经看到过这种“无限循环”套接字,这是监听套接字的套接字 ID。我们会检查这个监听套接字的状态。如果它的“位”被设置了,意味着有新的连接正在等待接入。换句话说,当某人尝试连接到我们的服务器时,这个监听套接字的对应位会被设置,指示有一个新连接在等待。 检查监听套接字要检查这个监听套接字的状态,我们可以使用宏 示例代码if (FD_ISSET(listenerSocket, &rfds)) {
// 说明有新的连接
} 在这段代码中,我们检查监听套接字是否已设置。如果设置了,表示有新连接需要接受。 接受新连接为了接受新连接,我们需要编写一个函数,例如 void AcceptNewConnection() {
int clientSocket = accept(listenerSocket, NULL, NULL);
if (clientSocket < 0) {
std::cout << "Error accepting new connection" << std::endl;
return;
}
// 成功接收到连接
} 限制最大客户端数在实现中,我们限制最多只能接受 5 个客户端。具体做法是检查当前客户端的连接数,并在达到上限时返回“忙碌”消息。代码如下: int clientSockets[5] = {0}; // 保存客户端套接字
int index = 0;
for (index = 0; index < 5; index++) {
if (clientSockets[index] == 0) {
break;
}
}
if (index == 5) {
std::cout << "Server is busy, try again later." << std::endl;
return;
} 新客户端接入如果没有达到最大客户端数,我们就可以接受新连接并分配一个可用的索引给它。 clientSockets[index] = clientSocket; 处理连接失败如果 if (clientSocket < 0) {
std::cout << "Failed to accept new connection" << std::endl;
return;
} 总结通过上述步骤,我们成功地实现了接受新连接的功能。新的客户端连接通过 5.5 开始与新客户端通信本视频概述在前一个视频中,我们学习了如何在监听套接字被设置的情况下接受新的连接。今天,我们将了解如何检查是否有任何已经连接的客户端正在向我们发送消息。 接收来自已连接客户端的消息为了检查现有客户端是否发送了消息,我们需要使用一个循环来检查每个客户端套接字是否有数据可读。这个过程的基本逻辑是:
遍历客户端套接字我们首先定义一个循环,遍历客户端套接字数组,并使用 示例代码:for (int index = 0; index < 5; index++) {
if (FD_ISSET(clientSockets[index], &rfds)) {
// 说明该客户端套接字有数据可以读取
ReceiveOrSend(clientSockets[index]);
}
} 在这段代码中,我们检查每个客户端套接字是否有数据准备好读取。如果有,则调用 接收消息的函数我们定义了一个 void ReceiveOrSend(int clientSocket) {
std::cout << "Receiving message from client..." << std::endl;
char buffer[255]; // 定义接收消息的缓冲区
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived < 0) {
// 出现错误时关闭套接字
std::cout << "Error receiving message from socket " << clientSocket << std::endl;
close(clientSocket);
clientSocket = 0; // 清空该套接字
} else if (bytesReceived == 0) {
// 客户端关闭了连接
std::cout << "Client disconnected, closing socket " << clientSocket << std::endl;
close(clientSocket);
clientSocket = 0; // 清空该套接字
} else {
// 成功接收消息
std::cout << "Message from client (" << clientSocket << "): " << buffer << std::endl;
// 发送响应给客户端
const char* response = "Acknowledgement from server";
int bytesSent = send(clientSocket, response, strlen(response), 0);
if (bytesSent < 0) {
std::cout << "Error sending message to socket " << clientSocket << std::endl;
close(clientSocket);
clientSocket = 0; // 清空该套接字
} else {
std::cout << "Acknowledgement sent to client (" << clientSocket << ")" << std::endl;
}
}
} 代码解析
清理工作在每次操作之后,我们清理并重置相应的客户端套接字数组中的套接字,以确保下次处理时没有留下无效的套接字。 if (clientSocket != 0) {
// 清理客户端套接字
clientSockets[index] = 0;
} 多客户端支持本视频中的代码是为支持多个客户端连接设计的。每个客户端连接都会分配一个独立的套接字,我们通过循环检查每个客户端套接字的状态,以确保可以处理多个客户端同时发送的消息。 总结在本视频中,我们学习了如何接收来自已连接客户端的消息,并向客户端发送响应。我们还讨论了如何处理套接字错误和关闭连接。我们的代码现在能够支持与多个客户端的通信。接下来,我们将在后续视频中继续编写客户端代码,并演示如何与服务器进行通信。 未来的计划
感谢观看,本视频到此为止! 5.6 客户端代码与服务器通信欢迎回来我们已经准备好了用于处理多个客户端的套接字服务器代码。在这段代码中,我们编写了两个函数:一个用于接受新连接,另一个用于发送和接收消息。我在这两个函数上都设置了断点。一个断点设置在 客户端代码这个客户端代码使用了相同的 9999 端口号,之前已经在讲解客户端与服务器通信时做过说明。这个代码我会在本节的最后视频中作为资源提供给你。在此,我只是简单过一遍,目的是让你了解客户端代码的基本结构。首先,客户端需要调用一些初始化方法,然后就像服务器端一样,客户端也会创建一个套接字,并设置相关的服务信息,如家庭、端口号、IP 地址等。这里我们硬编码了机器的 IP 地址,但实际上你应该避免硬编码,最好是从外部配置或数据库中读取服务器的 IP 地址。 客户端与服务器的连接一旦客户端设置完成,它会尝试连接到服务器。连接成功后,客户端会发送一个固定的测试消息给服务器。当服务器接收到消息时,它会向客户端发送一个确认消息。我们在之前的视频中已经写了这个代码来处理服务器如何发送确认消息。如果你对这些内容有疑问,可以回到之前的视频查看。 运行服务器我现在启动服务器。当服务器运行时,它会不断地等待连接,并显示没有新的连接消息。这个消息是因为 启动客户端现在,我启动客户端,客户端会在 服务器端的代码继续循环运行,在客户端成功连接并发送消息后,服务器开始接收并处理来自客户端的消息。服务器在接收到客户端的消息后,会打印出接收到的消息(在本例中是“固定文本消息”)。然后,服务器会向客户端发送确认消息。 服务器和客户端的通信客户端发送消息后,服务器接收并打印消息,并且发送一个确认消息。客户端收到这个确认消息后,显示“从服务器收到的确认消息”。服务器会在完成通信后关闭与客户端的连接。 服务器关闭连接当客户端关闭连接时,服务器会收到一个错误,表示该客户端的连接已被关闭。服务器此时会关闭套接字,清理资源。 多客户端处理在接下来的几期视频中,我们将继续扩展此示例,处理多个客户端同时与服务器通信。在下一期视频中,我将修改客户端代码,使其在循环中发送动态消息,这样客户端就能向服务器发送来自用户的输入消息。 记录客户端通信到文件此外,我还计划修改服务器端代码,将接收到的消息以及相关信息(如客户端套接字)写入文件中,而不是仅仅在控制台打印。这将帮助我们跟踪每个客户端的通信,并且能够更好地调试和查看消息内容。 总结到目前为止,我们已经完成了基本的服务器和客户端通信,能够处理单个客户端的消息并向客户端发送确认消息。在下一个视频中,我将继续扩展该示例,并添加多客户端支持以及日志记录功能。如果你有任何疑问,可以查看之前的视频,理解每个部分的工作原理。 下一个步骤在下一期视频中,我将展示如何处理多个客户端,并在服务器端记录消息到文件。你可以通过下载代码,自己运行并调试代码,帮助你更好地理解这些概念。 感谢观看,我们下期再见! 5.7 多客户端与单服务器通信演示修改客户端和服务器程序我们将对客户端和服务器程序进行一些小的修改。在服务器程序中,我们将做出以下变化:之前在客户端接收到消息时,我们会直接打印出来,而现在,我们不再在屏幕上打印消息,而是将消息写入到文件中。 在文件中记录消息为了实现这一点,我们将使用文件操作。首先,我们需要在程序中创建一个全局变量来存储文件指针, #include <stdio.h> 使用这个头文件,可以帮助我们打开文件并写入内容。我们将创建一个文件来存储客户端的消息。文件打开的位置将是在程序的某个位置,便于记录所有的消息。 程序启动时打开文件程序启动时,我们将做如下操作:在初始化任何套接字之前,先打开文件。实际上,建议在执行 打开文件的代码如下: f = fopen("MSG_client.txt", "a"); 我们打开文件时使用追加模式("a"),这样每次写入文件时不会覆盖原有内容,而是将新的消息追加到文件末尾。如果文件无法打开,程序会打印错误信息并退出。 if (f == NULL) {
printf("Error opening file.\n");
exit(1);
} 将客户端消息写入文件每当接收到来自客户端的消息时,我们会将该消息写入到文件中。代码如下: fprintf(f, "Message from client (socket %d): %s\n", client_socket, msg); 这里我们将客户端套接字和消息一起写入文件,以便之后追踪和查看哪个客户端发送了哪条消息。 客户端程序的修改对于客户端,我们做了一些修改,使其能定期发送消息。在原有的程序中,我们每次都会提示用户输入消息,而现在我们修改为每隔2秒发送一个固定的消息。这样可以模拟多个客户端同时连接到服务器的场景。 while (1) {
send_message("Fixed message");
sleep(2); // 每2秒发送一次消息
} 启动服务器和客户端修改完成后,我们启动服务器和客户端进行测试。首先,运行服务器程序,然后打开多个客户端实例,每个客户端都将在2秒间隔内向服务器发送消息。服务器会响应每个客户端的消息,并将其记录到文件中。 查看日志文件当服务器停止时,我们可以打开记录客户端消息的文件
每条消息后面都会有客户端的套接字ID,帮助我们跟踪哪个客户端发送了哪条消息。 总结通过这些改动,我们实现了将客户端发送的消息记录到文件中,这对于调试和日志记录非常有用。虽然写入文件会带来一定的性能开销,但在需要追踪网络通信的场景下,记录日志是非常有帮助的。 希望通过这个练习,您对套接字编程的理解能有所提高,并且能在实际开发中更好地处理客户端和服务器之间的通信。 |
6. UDP 套接字程序实践6.1 UDP 服务器代码欢迎回来,朋友们。在这个视频中,我们将看到如何创建一个基于UDP的服务器程序。到目前为止,我们已经看过了所有TCP的示例,现在我们将创建一个基于UDP协议的服务器,并在下一个视频中创建一个基于UDP协议的客户端程序。在理论部分,你可能已经了解了面向连接和无连接套接字之间的区别,你也可能理解了基于TCP和UDP的通信。那么,现在我们就来讨论基于UDP的通信,我们将在本视频中编写一个程序。 UDP协议的工作原理在数据报套接字(datagram socket)中,每次都不需要保持一个活动的连接。所以在基于数据报的UDP服务器中,你不需要像在TCP中那样维护特定的客户端信息。在UDP中,任何客户端发送的数据都可以被接收,并且你可以立即对该客户端作出响应,告知数据已被接收。 通常,UDP套接字用于广播应用程序,或者是那种任何客户端可以连接一次并接收你想要发送的信息的应用程序。这些应用程序通常包括实时系统,比如在线电视节目等,多个客户端可以连接并接收信息。在这些应用程序中,服务器就像是广播信息给任何连接到它的客户端。 创建UDP服务器我们来编写UDP服务器的代码。为了简化操作,我会使用和之前TCP程序相同的代码,并进行一些修改。首先,我包括了这些头文件: 在TCP协议中,我们使用了 初始化和库引用你需要引入相应的库,比如 主要的服务器代码在服务器程序中,我们需要使用 接下来,我们使用 套接字绑定与TCP服务器一样,UDP服务器也需要将套接字绑定到特定的端口。通过调用 if (bind(listenSocket, (SOCKADDR*)&localAddr, sizeof(localAddr)) == SOCKET_ERROR) {
std::cerr << "Failed to bind to local port." << std::endl;
return;
} 接收消息为了接收来自客户端的消息,我们使用 int bytesReceived = recvfrom(listenSocket, buffer, 255, 0, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (bytesReceived > 0) {
// 处理接收到的消息
} else {
std::cerr << "Error receiving data." << std::endl;
} 在这段代码中,我们指定了 发送回应接收到消息后,服务器需要向客户端发送一个确认消息。使用 const char* ackMessage = "ACK from server";
sendto(listenSocket, ackMessage, strlen(ackMessage), 0, (SOCKADDR*)&clientAddr, clientAddrLen); 运行循环由于我们是处理UDP通信,我们的服务器程序需要进入一个无限循环,随时准备接收来自任何客户端的消息。这意味着我们的程序会一直运行,直到被手动终止。 文件处理除了接收和发送数据,服务器还可以把接收到的消息写入文件中。在这里,我使用 FILE* fp = fopen("UDP_msg.txt", "a");
if (fp != NULL) {
fprintf(fp, "Message from client: %s\n", buffer);
fclose(fp);
} else {
std::cerr << "Error opening file." << std::endl;
} 错误处理在每个关键操作后,我们都会检查是否发生了错误。例如,在绑定、接收和发送数据时,我们都要检查操作是否成功。如果失败,我们会打印错误信息并退出程序。 代码示例以下是服务器的完整代码示例(简化版本): #include <iostream>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET listenSocket;
sockaddr_in localAddr, clientAddr;
char buffer[256];
int clientAddrLen = sizeof(clientAddr);
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
return 1;
}
// 创建 UDP 套接字
listenSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (listenSocket == INVALID_SOCKET) {
std::cerr << "Socket creation failed." << std::endl;
WSACleanup();
return 1;
}
// 设置本地地址
memset(&localAddr, 0, sizeof(localAddr));
localAddr.sin_family = AF_INET;
localAddr.sin_addr.s_addr = INADDR_ANY;
localAddr.sin_port = htons(8888);
// 绑定套接字
if (bind(listenSocket, (SOCKADDR*)&localAddr, sizeof(localAddr)) == SOCKET_ERROR) {
std::cerr << "Binding failed." << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
// 接收数据
while (true) {
int bytesReceived = recvfrom(listenSocket, buffer, 255, 0, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0'; // Null-terminate the string
std::cout << "Received message: " << buffer << std::endl;
// 写入文件
FILE* fp = fopen("UDP_msg.txt", "a");
if (fp != NULL) {
fprintf(fp, "Message from client: %s\n", buffer);
fclose(fp);
}
// 发送确认消息
const char* ackMessage = "ACK from server";
sendto(listenSocket, ackMessage, strlen(ackMessage), 0, (SOCKADDR*)&clientAddr, clientAddrLen);
}
}
// 清理
closesocket(listenSocket);
WSACleanup();
return 0;
} 结论在本视频中,我们创建了一个简单的UDP服务器,它能够接收来自客户端的消息,并且返回确认消息。你可以根据自己的需求进一步扩展此程序,比如加入更多的客户端处理逻辑、消息解析、错误处理等。 6.2 UDP 客户端代码欢迎回来!今天我们将编写UDP客户端的代码。在之前的视频中,我们讨论了需要包含哪些库、如何定义端口号以及需要使用哪些头文件。这些内容在今天的视频中仍然适用,我们依然会使用相同的头文件和库。 接下来让我们进入主函数,并再次提供这个部分。你需要为此定义一个套接字,使用SRB(结构体变量),因为这个结构体包含了关于服务器的信息,我们的UDP客户端将与此服务器进行交互。在主函数内部,你可以看到我们做了同样的事情:启动程序,这对于启动网络请求非常重要,能够将程序加载到内存中,以便可以使用套接字API定义。启动是任何套接字程序中都必须的,无论你编写什么类型的程序,Windows环境下都需要启动。 现在,我们来看一下如何打开套接字。首先这是我的客户端套接字。我们正在打开此套接字,首先需要使用第一个参数作为无限,第二个参数应该为数据报(datagram)。然后,协议我们需要使用TCP/IP,但对于数据报套接字,必须使用UDP。因此,常量应该是IP_UDP,而不是TCP。虽然TCP流套接字需要使用TCP协议,但数据报套接字必须使用UDP协议。 如果套接字API调用失败,它将返回-1。如果返回-1,你可以显示错误信息并返回失败。 接下来,你可以像在服务器端程序中那样提供细节,但需要记住的是,如果你编写的是客户端程序并且不会将套接字绑定到特定端口,那么你需要使用整数来查找正确的IP地址,这个IP地址是你的服务器所在的机器地址,而这个你是已经知道的。 对于TCP协议或连接导向的套接字(如流套接字),我们通常需要与服务器建立连接,但在UDP中,我们不需要连接服务器。我们可以直接开始向服务器发送和接收信息。首先,我们可以声明一个变量,例如名为 例如: sendto(socket, "test message", 13, 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); 这里,我们向服务器发送了一条长度为13的测试消息。如果发送失败,我们会收到一个返回值小于0的错误代码,表示发送失败。在这种情况下,我们可以显示错误信息并处理失败的情况。 接下来,我们需要接收来自服务器的确认信息。我们可以使用 例如: recvfrom(socket, buffer, 255, 0, NULL, NULL); 如果接收消息失败,同样我们会显示错误信息并退出程序。 关键代码结构:
在下一集视频中,我们将演示如何发送和接收测试消息,展示客户端如何与服务器通过UDP协议进行通信。 今天的视频内容到这里为止。我们将在下一集展示如何通过UDP协议实现客户端和服务器之间的通信。 6.3 客户端与服务器之间的通信演示欢迎回来!在本视频中,我们将展示如何运行之前编写的UDP客户端和服务器程序。你可以看到这是服务器程序,接下来我们将对其做一个小修改。在服务器端的 服务器端和客户端的变化:
运行流程:
运行过程演示:
客户端-服务器多客户端测试:
文件操作和调试:
运行多个客户端:
总结:
后续内容:
如果你按照本视频的示范进行了操作并成功实现了客户端和服务器的通信,你现在已经具备了中级编程的能力,能够处理基本的网络通信任务。 谢谢你的观看,我们在下一节视频中再见! |
7. 剩余部分7.1 第 1 部分 - gethostname 和 gethostbyname 使用欢迎回来!在本节中,我们将讨论一些在之前客户端和服务器程序示例中没有使用过,但它们非常有用的API。了解这些API的使用方法将非常有益,并且能帮助你更好地理解它们的作用及如何使用它们。今天我们将讨论两个API:
我们将一步步演示如何使用这些API,并在程序中实际应用它们。 相关设置和准备:
API详细讲解:1. gethostname:
2. gethostbyname:
代码演示:#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
int main() {
WSADATA wsaData;
char hostname[255];
struct hostent *he;
struct in_addr **addr_list;
// 初始化Winsock
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Unable to load socket library\n");
return 1;
}
// 获取主机名
if (gethostname(hostname, sizeof(hostname)) == 0) {
printf("Hostname: %s\n", hostname);
} else {
printf("Error getting hostname\n");
}
// 获取主机的IP地址
he = gethostbyname(hostname);
if (he == NULL) {
printf("Unable to get host by name\n");
return 1;
}
addr_list = (struct in_addr **)he->h_addr_list;
printf("IP Address: %s\n", inet_ntoa(*addr_list[0])); // 获取IPv4地址
// 清理并退出
WSACleanup();
return 0;
} 代码分析:
运行结果:假设程序运行在名为
这些API的应用场景:
小结:
下一个视频中,我将讨论与 7.2 第 2 部分 - send 和 recv 标志参数欢迎回来。在本视频中,我们将讨论之前在前面的视频中使用过的两个API。在前面的视频中,我们讨论了如何在客户端和服务器之间进行通信。在此过程中,我们使用了 Flags 参数首先,让我们看看 1.
|
C++ Socket套接字编程 Socket Programming In C++ (Windows)
以下是翻译:
The text was updated successfully, but these errors were encountered: