Java开发笔记(一百一十四)利用Socket传输文本消息
前面介绍了http协议的网络通信,包括接口调用、文件下载和文件上传,这些功能固然已经覆盖了常见的联网操作,可是http协议拥有专门的通信规则,这些规则一方面有利于维持正常的数据交互,另一方面不可避免地缺少灵活性,比如下列条条框框就难以逾越:
1、http连接属于短连接,每次访问操作结束之后,客户端便会关闭本次连接。下次还想访问接口的话,就得重新建立连接,要是频繁发生数据交互的话,反复的连接和断开将造成大量的资源消耗。
2、在http连接中,服务端总是被动接收消息,无法主动向客户端推送消息。倘若客户端不去请求服务端,服务端就没法发送即时消息。
3、每次http调用都属于客户端与服务端之间的一对一交互,完全与第三者无关(比如另一个客户端),这种技术手段无法满足类似qq聊天那种群发消息的要求。
4、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:判断套接字是否关闭。
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,很高兴认识你
同时观察到下面的服务端日志:
12:40:12.543 thread-0 接收文本的socket服务已启动
12:41:15.970 thread-1 收到客户端消息:hello world
12:41:15.971 thread-1 服务端返回消息:hi,很高兴认识你
根据以上的客户端日志以及服务端日志,可知通过socket成功实现了文本传输功能。
更多java技术文章参见《java开发笔记(序)章节目录》