Java开发笔记(一百一十四)利用Socket传输文本消息

前面介绍了HTTP协议的网络通讯,包括接口调用、文件下载和文件上传,这些功能当然已经覆盖了常见的联网操做,但是HTTP协议拥有专门的通讯规则,这些规则一方面有利于维持正常的数据交互,另外一方面不可避免地缺乏灵活性,好比下列条条框框就难以逾越:
一、HTTP链接属于短链接,每次访问操做结束以后,客户端便会关闭本次链接。下次还想访问接口的话,就得从新创建链接,要是频繁发生数据交互的话,反复的链接和断开将形成大量的资源消耗。
二、在HTTP链接中,服务端老是被动接收消息,没法主动向客户端推送消息。假若客户端不去请求服务端,服务端就无法发送即时消息。
三、每次HTTP调用都属于客户端与服务端之间的一对一交互,彻底与第三者无关(好比另外一个客户端),这种技术手段没法知足相似QQ聊天那种群发消息的要求。
四、HTTP链接须要搭建专门的HTTP服务器,这样的服务端比较重,不适合两个设备终端之间的简单信息传输。
诚然HTTP协议作不到如此灵活多变的地步,势必要在更基础的层次去实现变化无穷的场景。在Java编程中,网络通讯的基本操做单元实际上是套接字Socket,它自己不是什么协议,而是一种支持TCP/IP协议的通讯接口。建立Socket链接的时候,容许指定当前的传输层协议,当Socket链接的双方握手确认连上以后,此时采用的是TCP协议;当Socket链接的双方未确认连上就自顾自地发送数据,此时采用的是UDP协议。在TCP协议的实现过程当中,每次创建Socket链接至少须要一对套接字,其中一个运行于客户端,用的是Socket类;另外一个运行于服务端,用的是ServerSocket类。
Socket工具虽然主要用于客户端,但服务端一般也保留一份客户端的Socket备份,它描述了两边对套接字处理的通常行为。下面是Socket类的主要方法说明:
connect:链接指定IP和端口。该方法用于客户端链接服务端,成功连上以后才能开展数据交互。
getInputStream:获取套接字的输入流,输入流用于接收对方发来的数据。
getOutputStream:获取套接字的输出流,输出流用于向对方发送数据。
isConnected:判断套接字是否连上。
close:关闭套接字。套接字关闭以后将没法再传输数据。
isClosed:判断套接字是否关闭。html

ServerSocket仅用于服务端,它的构造函数可指定侦听指定端口,从而及时响应客户端的链接请求。下面是ServerSocket的主要方法说明:
accept:开始接收客户端的链接。一旦有客户端连上,就返回该客户端的套接字对象。若要持续侦听链接,得在循环语句中调用该方法。
close:关闭服务端的套接字。
isClosed:判断服务端的套接字是否关闭。编程

因为套接字属于长链接,只要链接的双方未调用close方法,也没退出程序运行,那么理论上都处于已链接的状态。既然是长时间链接,在此期间的任什么时候刻均可能发送和接收数据,为此套接字的客户端须要给每一个链接分配两个线程,其中一个线程专门用来向服务端发送信息,而另外一个线程专门用于从服务端接收信息。而服务端须要循环调用accept方法,以便持续侦听客户端的套接字请求,一旦接到某个客户端的链接请求,就开启一个分线程单独处理该客户端的信息交互。
接下来看个利用Socket传输文本消息的例子,为方便起见,每次只传输一行文本。因为要求I/O流支持读写一行文本,所以采用的输入流成员为缓存读取器BufferedReader,输出流成员为打印流PrintStream,其中前者的readLine方法可以读出一行文本,后者的println方法可以写入一行文本。据此编写的套接字客户端主要代码示例以下:缓存

//定义一个文本发送任务
public class SendText implements Runnable {
	// 如下为Socket服务器的IP和端口,根据实际状况修改
	private static final String SOCKET_IP = "192.168.1.8";
	private static final int TEXT_PORT = 51000; // 文本传输专用端口
	private BufferedReader mReader; // 声明一个缓存读取器对象
	private PrintStream mWriter; // 声明一个打印流对象
	private String mRequest = ""; // 待发送的文本内容

	@Override
	public void run() {
		Socket socket = new Socket(); // 建立一个套接字对象
		try {
			// 命令套接字链接指定地址的指定端口,超时时间为3秒
			socket.connect(new InetSocketAddress(SOCKET_IP, TEXT_PORT), 3000);
			// 根据套接字的输入流构建缓存读取器
			mReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			// 根据套接字的输出流构建打印流对象
			mWriter = new PrintStream(socket.getOutputStream());
			// 利用Lambda表达式简化Runnable代码。启动一条子线程从服务器读取文本消息
			new Thread(() -> handleRecv()).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	// 发送文本消息
	public void sendText(String text) {
		mRequest = text;
		// 利用Lambda表达式简化Runnable代码。启动一条子线程向服务器发送文本消息
		new Thread(() -> handleSend(text)).start();
	}

	// 处理文本发送事件。为了不多线程并发产生冲突,这里添加了synchronized使之成为同步方法
	private synchronized void handleSend(String text) {
		PrintUtils.print("向服务器发送消息:"+text);
		try {
			mWriter.println(text); // 往打印流对象中写入文本消息
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	// 处理文本接收事件。为了不多线程并发产生冲突,这里添加了synchronized使之成为同步方法
	private synchronized void handleRecv() {
		try {
			String response;
			// 持续从服务器读取文本消息
			while ((response = mReader.readLine()) != null) {
				PrintUtils.print("服务器返回消息:"+response);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

至于套接字的服务端,在accept方法侦听到客户端链接以后,使用的I/O流依然为缓存读取器BufferedReader与打印流PrintStream,为方便观察客户端和服务端的交互过程,服务端准备在接收客户端消息以后马上返回一行文本,从而告知客户端已经收到消息了。据此编写的套接字服务端主要代码示例以下:服务器

//定义一个文本接收任务
public class ReceiveText implements Runnable {
	private static final int TEXT_PORT = 51000; // 文本传输专用端口

	@Override
	public void run() {
		PrintUtils.print("接收文本的Socket服务已启动");
		try {
			// 建立一个服务端套接字,用于监听客户端Socket的链接请求
			ServerSocket server = new ServerSocket(TEXT_PORT);
			while (true) { // 持续侦听客户端的链接
				// 收到了某个客户端的Socket链接请求,并得到该客户端的套接字对象
				Socket socket = server.accept();
				// 启动一个服务线程负责与该客户端的交互操做
				new Thread(new ServerTask(socket)).start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	// 定义一个伺候任务,好生招待这位顾客
	private class ServerTask implements Runnable {
		private Socket mSocket; // 声明一个套接字对象
		private BufferedReader mReader; // 声明一个缓存读取器对象

		public ServerTask(Socket socket) throws IOException {
			mSocket = socket;
			// 根据套接字的输入流构建缓存读取器
			mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
		}

		@Override
		public void run() {
			try {
				String request;
				// 循环不断地从Socket中读取客户端发送过来的文本消息
				while ((request = mReader.readLine()) != null) {
					PrintUtils.print("收到客户端消息:" + request);
					// 根据套接字的输出流构建打印流对象
					PrintStream ps = new PrintStream(mSocket.getOutputStream());
					String response = "hi,很高兴认识你";
					PrintUtils.print("服务端返回消息:" + response);
					ps.println(response); // 往打印流对象中写入文本消息
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

接着服务端程序开启Socket专用的文本接收线程,线程启动代码以下所示:网络

		// 启动一个文本接收线程
		new Thread(new ReceiveText()).start();

而后客户端程序也开启Socket链接的文本发送线程,并命令该线程前后发送两条文本消息,消息发送代码以下所示:多线程

	// 发送文本消息
	private static void testSendText() {
		SendText task = new SendText(); // 建立一个文本发送任务
		new Thread(task).start(); // 为文本发送任务开启分线程
		task.sendText("你好呀"); // 命令该线程发送文本消息
		task.sendText("Hello World"); // 命令该线程发送文本消息
	}

 

最后完整走一遍流程,先运行服务端的测试程序,再运行客户端的测试程序,观察到的客户端日志以下:并发

12:41:15.967 Thread-3 向服务器发送消息:Hello World
12:41:15.972 Thread-2 服务器返回消息:hi,很高兴认识你

 

同时观察到下面的服务端日志:socket

12:40:12.543 Thread-0 接收文本的Socket服务已启动
12:41:15.970 Thread-1 收到客户端消息:Hello World
12:41:15.971 Thread-1 服务端返回消息:hi,很高兴认识你

 

根据以上的客户端日志以及服务端日志,可知经过Socket成功实现了文本传输功能。ide



更多Java技术文章参见《Java开发笔记(序)章节目录函数

相关文章
相关标签/搜索