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. 参考


  1. Java 创建线程的三种方式,博客园,2021。
  2. 通信编程:Winsock socket 编程步骤与样例,51CTO 博客,2021。
  3. Windows平台简单套接字编程,CSDN,2019。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值