欢迎访问我的博客首页。
Windows 网络编程
1. 交替收发
客户端逐个发送 1 到 20 内的单数,间隔为一秒,服务器收到消息马上发回给客户端。客户端断开后可以重新连接服务器,直到客户端发送 stop 后服务器关闭。由于是交替收发,不用考虑粘包问题。
1.1 客户端
Python 实现的客户端。
# encoding=utf-8
import time
import socket
if __name__ == '__main__':
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_socket.connect(('127.0.0.1', 12581))
for i in range(1, 20, 2):
# 发送消息。
data_send = str(i)
tcp_socket.send(data_send.encode())
print('已发送消息:' + data_send + '。')
# 接收消息。
data_recv = tcp_socket.recv(1024)
if not data_recv:
break
print('接收到消息:' + data_recv.decode() + '。')
time.sleep(1)
tcp_socket.close()
1.2 服务器
accept 从监听队列中取出一个连接请求,监听队列为空时 accept 函数处于阻塞状态。read/recv 函数监听套接字,没有消息时该函数也处于阻塞状态。
python 实现的服务器。
# encoding=utf-8
import socket
if __name__ == '__main__':
serverSocket = socket.socket()
serverSocket.bind(('127.0.0.1', 12581))
serverSocket.listen()
# 循环等待连接。
goon = True
while goon:
# 接受连接请求。
connection, clientAddr = serverSocket.accept()
# 传输数据。
while goon:
# 接收
data = connection.recv(1024).decode()
# 客户端私自断开。
if not data:
print('客户端私自断开。')
break
# 客户端请求断开。
if data == 'stop':
goon = False
break
print(data)
# 发送。
connection.send(data.encode())
connection.close()
serverSocket.close()
java 实现的服务器。
package socket;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
public class javascoket {
public static void main(String[] args) throws IOException {
// 1. 创建套接字。
ServerSocket serverSocket = new ServerSocket(12581);
// 2. 循环等待连接。
boolean goon = true;
while (goon) {
// 2.1 从监听队列中取出连接请求。
Socket tcp_socket = serverSocket.accept();
if (tcp_socket.isConnected()) {
System.out.println("已建立连接。");
} else {
System.out.println("连接失败。");
continue;
}
// 2.2 获取连接输入输出流。
BufferedInputStream bi = new BufferedInputStream(tcp_socket.getInputStream());
BufferedOutputStream bo = new BufferedOutputStream(tcp_socket.getOutputStream());
// 2.3 传输数据。
int len = 0;
byte[] data = new byte[1024];
while (goon) {
// 接收消息。
len = bi.read(data);
// 客户端私自断开。
if (len == -1) {
System.out.println("客户端私自断开。");
break;
}
String msg = new String(data, 0, len);
// 客户端请求断开。
if (msg.equals("stop")) {
goon = false;
break;
}
System.out.println(msg);
// 发送消息。
bo.write(data);
bo.flush();
}
// 2.4 关闭输入输出流。
bi.close();
bo.close();
}
// 3. 关闭套接字。
serverSocket.close();
}
}
C++ 实现的服务器,用于 Windows 系统。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
// 1. 启动 windows 异步套接字。
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 2. 设置服务器 IP 地址。
SOCKADDR_IN serverAddr;
serverAddr.sin_addr.S_un.S_addr = INADDR_ANY;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(12581);
// 3. 创建服务器端套接字并绑定 IP 地址。设置监听队列长度,进入监听状态。
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
listen(serverSocket, 1);
// 4. 循环等待连接。
bool goon = true;
while (goon) {
// 4.1 寻址客户端,接受连接请求。
SOCKADDR_IN clientAddr;
int len = sizeof(clientAddr);
SOCKET clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &len);
// 4.2 传输数据。
size_t size;
char data[100];
while (goon) {
memset(data, 0, sizeof(data));
// 接收。
size = recv(clientSocket, data, sizeof(data), 0);
// 客户端私自断开。
if (size == 0) {
std::cout << "客户端私自断开。" << std::endl;
break;
}
// 客户端请求断开。
if (strcmp(data, "stop") == 0) {
goon = false;
break;
}
std::cout << data << std::endl;
// 发送。
send(clientSocket, data, strlen(data), 0);
}
// 4.3 关闭连接请求。
closesocket(clientSocket);
}
// 5. 关闭套接字。
closesocket(serverSocket);
WSACleanup();
return 0;
}
2. 粘包问题
连续发送的字节流可能会粘连在一起,这称为粘包问题。解决粘包问题的方法是,先发送数据的长度,再发送数据的内容。下面的程序中,服务器连续发送 1、10、100、1000、10000,客户端只接收不发送。
2.1 客户端
python 实现的客户端。
# encoding=utf-8
import time
import socket
if __name__ == '__main__':
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_socket.connect(('127.0.0.1', 12581))
while True:
# 接收消息。
data_length = tcp_socket.recv(4)
if not data_length:
break
data_content = tcp_socket.recv(int.from_bytes(data_length, 'big'))
print(data_content.decode())
time.sleep(1)
tcp_socket.close()
2.2 服务器
java 实现的服务器。
package socket;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.io.BufferedOutputStream;
public class javascoket {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(12581);
Socket tcp_socket = serverSocket.accept();
if (tcp_socket.isConnected()) {
System.out.println("已建立连接。");
} else {
System.out.println("连接失败。");
}
BufferedOutputStream bo = new BufferedOutputStream(tcp_socket.getOutputStream());
byte[] data_content;
byte[] data_length;
for (int i = 1; i < 100000; i *= 10) {
data_content = Integer.toString(i).getBytes();
data_length = int2byte(data_content.length);
bo.write(data_length);
bo.write(data_content);
bo.flush();
System.out.println("已发送消息:" + i + "。");
}
bo.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("程序结束。");
}
public static byte[] int2byte(int i) {
byte[] result = new byte[4];
result[0] = (byte) ((i >> 24) & 0xFF);
result[1] = (byte) ((i >> 16) & 0xFF);
result[2] = (byte) ((i >> 8) & 0xFF);
result[3] = (byte) (i & 0xFF);
return result;
}
}
由于 Java 中的整型转换为字节数组后占 4 字节,所以客户端先读取 4 字节转换回整型,得到数据长度,再根据这个长度读取数据部分。
3. 双工通信
借助多线程实现双工通信,客户端和服务器中各使用两个线程完成读写操作。发送方发送的数据长度为 0 时,接收方认为读取结束。
在 Java 中,读写流任意一个的 close 函数被调用,连接都会断开。所以服务器中使用共享变量 ioStream 存放读写流,当读写线程都结束后才调用读写流的 close 函数。
客户端:
# encoding=utf-8
import socket
import threading
import random
import time
def reader(my_socket):
while True:
# 1. 获取数据长度。
data_length = my_socket.recv(4)
length = int.from_bytes(data_length, 'big')
if length == 0:
break
# 2. 读取数据内容。
data_content = my_socket.recv(length)
print('收到消息:' + data_content.decode() + '。')
time.sleep(0.1 * random.randint(0, 10))
def writer(my_socket):
it = 10
for i in range(it):
# 1. 准备消息内容。
msg = 'client_' + str(random.randint(1, 1000))
print('发送消息:' + msg + '。')
# 2. 字节流形式的消息长度、消息内容。
data_content = msg.encode()
data_length = len(data_content).to_bytes(4, 'big')
# 3. 发送。
my_socket.send(data_length)
my_socket.send(data_content)
time.sleep(0.1 * random.randint(0, 10))
if __name__ == '__main__':
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
tcp_socket.connect(('127.0.0.1', 12581))
thread1 = threading.Thread(target=reader, args=(tcp_socket,))
thread2 = threading.Thread(target=writer, args=(tcp_socket,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
# 读写完成,告诉客户端断开连接。
tcp_socket.send((0).to_bytes(4, 'big'))
tcp_socket.close()
服务器:
package socket;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.util.Random;
public class javascoket {
public static void main(String[] args) {
try {
// 1. 等待连接。
ServerSocket serverSocket = new ServerSocket(12581);
Socket tcp_socket = serverSocket.accept();
if (tcp_socket.isConnected()) {
System.out.println("已建立连接。");
} else {
System.out.println("连接失败。");
System.exit(0);
}
// 2. 读写流。
IOStream ioStream = new IOStream(tcp_socket);
// 3. 负责读写的两个线程,共享读写流。
Reader reader = new Reader(ioStream);
Writer writer = new Writer(ioStream);
reader.start();
writer.start();
reader.join();
writer.join();
// 4. 断开连接。
System.out.println("关闭套接字。");
ioStream.bi.close();
ioStream.bo.close();
serverSocket.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束。");
}
}
class IOStream {
public IOStream(Socket socket) throws IOException {
bi = new BufferedInputStream(socket.getInputStream());
bo = new BufferedOutputStream(socket.getOutputStream());
}
BufferedInputStream bi;
BufferedOutputStream bo;
}
class Reader extends Thread {
// 1. 构造函数。
public Reader(IOStream ioStream) {
bi = ioStream.bi;
}
// 2. 重写的线程执行函数。
public void run() {
try {
Random rd = new Random();
while (true) {
// 2.1 读取数据长度。
bi.read(data_length);
length = byte2int(data_length);
if (length == 0) {
System.out.println("应客户端要求,断开连接。");
break;
}
// 2.2 读取数据内容。
data_content = new byte[length];
bi.read(data_content);
System.out.println("收到消息:" + new String(data_content, 0, length) + "。");
Thread.sleep(rd.nextLong(1000));
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
// 3. 字节数组转整型。
public static int byte2int(byte[] b) {
int res = 0;
for (int i = 0; i < b.length; i++) {
res += (b[i] & 0xff) << ((3 - i) * 8);
}
return res;
}
// 4. 数据成员。
BufferedInputStream bi;
byte[] data_length = new byte[4];
int length = 0;
byte[] data_content;
}
class Writer extends Thread {
// 1. 构造函数。
public Writer(IOStream ioStream) {
bo = ioStream.bo;
}
// 2. 重写的线程执行函数。
public void run() {
try {
Random rd = new Random();
for (int i = 0; i < 10; i++) {
msg = "server_" + Integer.toString(rd.nextInt(1000));
System.out.println("发送消息:" + msg + "。");
data_content = msg.getBytes();
data_length = int2byte(data_content.length);
bo.write(data_length);
bo.write(data_content);
bo.flush();
Thread.sleep(rd.nextLong(1000));
}
// 告诉客户端发送完毕。
bo.write(int2byte(0));
bo.flush();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
// 3. 整型转字节数组。
public static byte[] int2byte(int i) {
byte[] res = new byte[4];
res[0] = (byte) ((i >> 24) & 0xFF);
res[1] = (byte) ((i >> 16) & 0xFF);
res[2] = (byte) ((i >> 8) & 0xFF);
res[3] = (byte) (i & 0xFF);
return res;
}
// 4. 数据成员。
BufferedOutputStream bo;
String msg;
byte[] data_content;
byte[] data_length;
}
4. 附录
4.1 Java 中 int 与 byte 数组互转
Java 语言实现整型与字节数组相互转换。
// int 转 byte[] 低字节在前(低字节序)
public static byte[] toLH(int n) {
byte[] b = new byte[4];
b[0] = (byte) (n & 0xff);
b[1] = (byte) (n >> 8 & 0xff);
b[2] = (byte) (n >> 16 & 0xff);
b[3] = (byte) (n >> 24 & 0xff);
return b;
}
// int 转 byte[] 高字节在前(高字节序)
public static byte[] toHH(int n) {
byte[] b = new byte[4];
b[3] = (byte) (n & 0xff);
b[2] = (byte) (n >> 8 & 0xff);
b[1] = (byte) (n >> 16 & 0xff);
b[0] = (byte) (n >> 24 & 0xff);
return b;
}
// byte[] 转 int 低字节在前(低字节序)
public int toInt(byte[] b){
int res = 0;
for(int i=0;i<b.length;i++){
res += (b[i] & 0xff) << (i*8);
}
return res;
}
// byte[] 转 int 高字节在前(高字节序)
public static int toInt(byte[] b){
int res = 0;
for(int i=0;i<b.length;i++){
res += (b[i] & 0xff) << ((3-i)*8);
}
return res;
}
5. 参考
- Java 创建线程的三种方式,博客园,2021。
- 通信编程:Winsock socket 编程步骤与样例,51CTO 博客,2021。
- Windows平台简单套接字编程,CSDN,2019。