🎈正式进入Socket的学习,学习完Socket的相关知识后,对以后得网络开发会更加得心应手。🎈
先简单回顾一下 TCP/IP 网络层次模型:
- 套接字(Socket)是在应用层和传输层之间的一个接口,用于实现网络通信。它并不属于计算机网络的特定层次,而是在网络编程中使用的一种编程接口。
- 套接字本身并不是一种协议,而是通过网络传输层协议(如TCP或UDP)来实现数据的传输。套接字提供了一组标准的函数接口,使得应用程序能够方便地进行网络通信操作,而不需要关注底层协议的细节。
- 一个Socket是一对IP地址和端口。Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。你可以这么理解:socket是进程之间用来对话的中间层工具。
应用进程使用传输层提供的服务才能够交换报文,实现应用层协议,实现应用。
两个应用进程需要借助 Socket API 使用传输层向应用层提供的传输服务来实现报文的交换。
TCP(传输控制协议)和 UDP(用户数据报协议)是两种常用的网络传输协议,它们在套接字编程中使用不同的方式进行网络通信。
- TCP Socket 使用 TCP 协议提供可靠的、面向连接的、字节流的服务,适用于需要确保数据完整性和顺序的应用场景。
- UDP Socket 使用 UDP 协议提供不可靠(数据UDP数据报)、无连接的服务,适用于实时性要求较高、数据丢失可接受的应用场景。
套接字:应用进程与端到端传输协议(TCP或UDP)之间的门户
TCP服务:从一个进程向另一个进程可靠地传输字节流。
使用TCP Socket编程,客户端和服务器端需要分别进行以下操作:
- 客户端
- 创建一个TCP套接字对象。目的也是返回一个整数,隐式的绑定本地的IP和端口(不需要我们自己调用)。
- 连接到服务器:使用套接字对象的connect函数,指定服务器的IP地址和端口,发起与服务器的连接请求。如果连接不到服务器就会一直阻塞等待,如果连接成功则继续向下执行。
- 进行数据交换:通过套接字对象的send和receive函数,向服务器发送数据并接收服务器的响应数据。
- 关闭连接:当通信完成或需要关闭连接时,调用套接字的close函数来关闭连接。
- 服务器端
- 创建一个TCP套接字对象。目的是返回一个整数(套接字就是一个整数)。
- 绑定地址和端口:将套接字绑定到服务器的本地IP地址和端口上。(这里的socket叫做welcome socket)
- 监听连接请求:调用套接字的listen函数,将套接字置于监听状态,等待客户端的连接请求。
- 接受连接:当有客户端发起连接请求时,调用套接字的accept函数来接受连接,创建一个新的套接字对象,用于与该客户端进行通信。如果没有客户端连接,则程序会阻塞等待(一直循环)。如果有一个客户端连接成功,则会再次创建一个connect socket,这个connect socket与服务器的IP与Port和客户端的IP与Port捆绑。(connect socket 与 welcome socket是不同的)
- 进行数据交换:通过connect socket的send和receive函数,与客户端进行数据的发送和接收操作。
- 关闭连接:当通信完成或需要关闭连接时,调用connect socket的close函数来关闭连接。(此时,connect socket就关闭,welcome socket继续等待其他客户端连接)
拓展:上面的过程一个服务器只能服务一个客户端,局限性很大。所以在接受到客户端连接后,可以创建一个新的线程去处理该客户端的业务。然后主线程继续等待其他客户端的连接。这样就可以实现一个服务器并发服务多个客户端。
使用UDP Socket编程,客户端和服务器端需要分别进行以下操作:
- 客户端:
- 创建一个UDP套接字对象。
- 发送数据:通过套接字对象的send to函数,将数据发送给服务器的IP地址和端口。
- 接收响应数据:通过套接字对象的recvfrom函数,接收服务器发送回来的响应数据。
- 关闭套接字:当通信完成或需要关闭套接字时,调用套接字的close函数来关闭套接字。
- 服务器端:
- 创建一个UDP套接字对象。
- 绑定地址和端口:将套接字绑定到服务器的本地IP地址和端口上。
- 接收数据:通过套接字对象的recvfrom函数,接收来自客户端的数据。
- 处理请求并发送响应:根据接收到的数据进行相应的处理,并通过套接字对象的send to函数,将响应数据发送给客户端的IP地址和端口。
- 关闭套接字:当通信完成或需要关闭套接字时,调用套接字的close函数来关闭套接字。
需要注意的是,UDP套接字编程的特点是无连接、不可靠,因此在数据交换过程中可能会存在数据丢失或乱序的情况。而TCP套接字编程是基于连接的、可靠的传输方式。具体的实现方式会因所使用的编程语言和套接字接口的不同而有所差异。以上是一般情况下的操作流程,具体的实现细节可能会有所不同。
- 本文使用QT库实现TCP Socket编程案例(下面还有C++版本的代码,由于QT封装了许多函数所以看起来代码很简洁)
- 在C/S模式下,服务器肯定是先运行的,客户端再去连接。所以本案例会制作一个服务器程序,还有一个客户端程序。
功能如下:
- 客户端连接服务器,并绑定信号槽(readyRead信号),用于处理服务器主动给客户端发送消息。
- 客户端给服务器主动发送消息
- 客户端关闭TCP Socket连接。
- 服务器监听端口,等待客户端连接。
- 客户端连接成功后,绑定信号槽(readyRead信号),用于处理客户端给服务器主动发动消息。
- 服务器主动给客户端发送消息
这个功能十分简单,但是代码还是比较繁杂,所以直接把工程分享出来,一个是客户端的工程,一个是服务器的工程。
代码的百度网盘链接放这里了:百度网盘程序链接 解压码:39b0
客户端界面
服务器
工程使用,拿回去之后,选择一个编译器编译一遍,然后先启动服务器,再启动客户端进行连接。(先后顺序不能弄错)
Qt使用socket制作的服务器可以连接多个客户端,并且通常使用了多线程技术来实现这一点。在传统的单线程服务器中,每个客户端连接都会阻塞服务器的主线程,导致服务器无法同时处理其他客户端的请求。 而使用多线程技术,可以为每个客户端连接创建一个独立的线程,使得服务器可以同时处理多个客户端的请求。
在Qt中,你可以使用QTcpServer类来创建基于TCP的服务器,并使用QTcpSocket类来处理与客户端的通信。当有新的客户端连接请求时,服务器会创建一个新的QTcpSocket对象,并将其放入一个独立的线程中处理。这样,每个客户端连接都有一个独立的线程用于处理通信,从而实现了多客户端的连接。
下面是一个简单的示例代码,展示了如何使用Qt创建一个多线程服务器
// 创建服务器对象
QTcpServer server;
// 监听指定端口
if (!server.listen(QHostAddress::Any, 8888)) {
qDebug() << "Server could not start!";
return;
}
// 服务器接受新连接时触发的槽函数
void MyClass::newConnection() {
// 获取新连接的套接字
QTcpSocket* socket = server.nextPendingConnection();
// 创建新的线程
QThread* thread = new QThread();
// 将套接字移到新线程中
socket->moveToThread(thread);
// 连接套接字的信号和槽函数
connect(socket, SIGNAL(readyRead()), this, SLOT(readMessage()));
connect(socket, SIGNAL(disconnected()), thread, SLOT(quit()));
// 启动线程
thread->start();
}
// 处理接收到的消息的槽函数
void MyClass::readMessage() {
QTcpSocket* socket = qobject_cast<QTcpSocket*>(sender());
if (socket) {
QByteArray data = socket->readAll();
// 处理接收到的消息的逻辑
}
}
在这个示例中,newConnection()函数作为服务器接受新连接时触发的槽函数,创建一个新的线程,并将新连接的套接字移动到该线程中。然后,通过连接套接字的信号和槽函数,可以在新线程中处理接收到的消息。
下面这个工程案例,服务器一次只能连接一个客户端,如果需要连接多个客户端,还需要使用多线程技术。
服务器端代码:
// TCPSocketServer.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cout << "Failed to initialize winsock" << std::endl;
return 1;
}
// 1.创建服务器端socket,此处的 serverSocket 变量对应文章中的welcome socket。
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
std::cout << "Failed to create server socket" << std::endl;
WSACleanup();
return 1;
}
// 设置服务器地址和端口
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = INADDR_ANY; // 使用任意可用的IP地址
serverAddress.sin_port = htons(12345); // 设置端口号为12345
// 2.绑定socket到服务器地址和端口
if (bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
std::cout << "Failed to bind server socket" << std::endl;
closesocket(serverSocket);
WSACleanup();
return 1;
}
// 3.监听连接请求
if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
std::cout << "Failed to listen on server socket" << std::endl;
closesocket(serverSocket);
WSACleanup();
return 1;
}
std::cout << "Server started, waiting for client connections..." << std::endl;
// 4.接受客户端连接,此处创建的 clientSocket 套接字对应文章中的 connect socket
SOCKET clientSocket = accept(serverSocket, NULL, NULL);
if (clientSocket == INVALID_SOCKET) {
std::cout << "Failed to accept client connection" << std::endl;
closesocket(serverSocket);
WSACleanup();
return 1;
}
std::cout << "Client connected" << std::endl;
// 5.接收和发送数据
char buffer[4096];
while (true) {
// 接收客户端数据
memset(buffer, 0, sizeof(buffer));
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived <= 0) {
std::cout << "Client disconnected" << std::endl;
break;
}
std::cout << "Received: " << buffer << std::endl;
// 发送响应给客户端
send(clientSocket, buffer, bytesReceived, 0);
}
// 6.关闭socket和清理Winsock
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}
客户端代码:
// TCPSocketClient.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main() {
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cout << "Failed to initialize winsock" << std::endl;
return 1;
}
// 1.创建客户端socket
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
std::cout << "Failed to create client socket" << std::endl;
WSACleanup();
return 1;
}
// 设置服务器地址和端口
sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(12345); // 设置服务器端口号为12345
if (inet_pton(AF_INET, "127.0.0.1", &(serverAddress.sin_addr)) <= 0) {
std::cout << "Invalid server address" << std::endl;
closesocket(clientSocket);
WSACleanup();
return 1;
}
// 2.连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) == SOCKET_ERROR) {
std::cout << "Failed to connect to server" << std::endl;
closesocket(clientSocket);
WSACleanup();
return 1;
}
std::cout << "Connected to server" << std::endl;
// 3.发送和接收数据
char buffer[4096];
std::string message;
// 循环等待
while (true) {
// 从用户输入读取消息
std::cout << "Enter a message (or 'q' to quit): ";
std::getline(std::cin, message);
if (message == "q") {
break;
}
// 发送消息给服务器
if (send(clientSocket, message.c_str(), message.size(), 0) == SOCKET_ERROR) {
std::cout << "Failed to send message to server" << std::endl;
break;
}
// 接收服务器响应
memset(buffer, 0, sizeof(buffer));
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived <= 0) {
std::cout << "Server disconnected" << std::endl;
break;
}
std::cout << "Server response: " << buffer << std::endl;
}
// 4.关闭socket和清理Winsock
closesocket(clientSocket);
WSACleanup();
return 0;
}