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开发笔记(序)章节目录》
Java开发笔记(一百一十四)利用Socket传输文本消息的更多相关文章
- Java开发笔记(三十四)字符串的赋值及类型转换
不管是基本的char字符型,还是包装字符类型Character,它们的每个变量只能存放一个字符,无法满足对一串字符的加工.为了能够直接操作一连串的字符,Java设计了专门的字符串类型String,该类 ...
- Java开发笔记(八十)利用反射技术操作私有方法
前面介绍了如何利用反射技术读写私有属性,不单是私有属性,就连私有方法也能通过反射技术来调用.为了演示反射的逆天功能,首先给Chicken鸡类增加下列几个私有方法,简单起见弄来了set***/get** ...
- Java开发笔记(八十四)文件与目录的管理
程序除了处理内存中的数据结构,还要操作磁盘上的各类文件,这里的磁盘是个统称,泛指可以持久保留数据的存储介质,包括但不限于:插在软驱中的软盘.固定在机箱中的硬盘.插在光驱中的光盘.插在USB接口上的U盘 ...
- Java开发笔记(六十四)静态方法引用和实例方法引用
前面介绍了方法引用的概念及其业务场景,虽然在所列举的案例之中方法引用确实好用,但是显而易见这些案例的适用场合非常狭窄,因为被引用的方法必须属于外层匿名方法(即Lambda表达式)的数据类型,像isEm ...
- Java开发笔记(二十四)方法的组成形式
经过前面的学习,我们发现演示的Java代码越来越复杂,而且每个例子的代码都堆在入口方法main内部,这会导致如下问题:1.一个方法内部堆砌了太多的代码行,看着费神,维护起来也吃力:2.部分代码描述的是 ...
- Java开发笔记(五十四)内部类和嵌套类
通常情况下,一个Java代码文件只定义一个类,即使两个类是父类与子类的关系,也要把它们拆成两个代码文件分别定义.可是有些事物相互之间密切联系,又不同于父子类的继承关系,比如一棵树会开很多花朵,这些花儿 ...
- Java开发笔记(七十四)内存溢出的两种错误
前面介绍的几种异常,其实都存在这样那样的逻辑问题,属于程序员的编码手误.还有一大类系统错误,表面上看不出什么问题,但是程序仍然运行不下去,兹举二例说明.第一个例子且看下列的测试代码: // 测试内存溢 ...
- OpenCV开发笔记(六十四):红胖子8分钟带你深入了解SURF特征点(图文并茂+浅显易懂+程序源码)
若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...
- Java开发微信公众号(四)---微信服务器post消息体的接收及消息的处理
在前几节文章中我们讲述了微信公众号环境的搭建.如何接入微信公众平台.以及微信服务器请求消息,响应消息,事件消息以及工具处理类的封装:接下来我们重点说一下-微信服务器post消息体的接收及消息的处理,这 ...
随机推荐
- Statistical Methods for Machine Learning
机器学习中的统计学方法. 从机器学习的核心视角来看,优化(optimization)和统计(statistics)是其最最重要的两项支撑技术.统计的方法可以用来机器学习,比如:聚类.贝叶斯等等,当然机 ...
- 解决IE报错[vue router]Failed to resolve async component default:strict 模式下不允许分配到只读属性
之前遇到过一个奇怪的问题,在其他浏览器下一切正常,但在万恶的IE下,却一直不行. 具体问题场景就是:比如orderDetail页面出现问题,那么只要是路由跳转的,点第1次无法跳转,必须得点第2次才可以 ...
- Java 堆栈内存的理解
Java中变量在内存中的分配1). 类变量(static修饰的变量):在程序加载时系统就为它在堆中开辟了内存,堆中的内存地址存放于栈以便高速访问.静态变量的生命周期—一直持续到整个“系统”关闭 2). ...
- 「PKUSC2018」PKUSC
传送门 Solution 考虑求每个点的贡献 等价于一个以OA长为半径的圆心为原点的圆在多边形内的弧对应的角度/\(2\pi\) 求弧度可以利用三角剖分 在原点的点要特判,采用射线法就可以了 Cod ...
- git常用命名:自用,持续更新
1.切换分支 git checkout -b dev origin/feature/迭代1.1 2.提交本地代码到github git init //初始化git git config --globa ...
- csp退役前的做题计划1(真)
csp退役前的做题计划1(真) 因为我太菜了,所以在第一次月考就会退役,还是记录一下每天做了什么题目吧. 任务计划 [ ] Z算法(Z Algorithm) 9.28 [x] ARC061C たくさん ...
- 用gmsh做前处理
原视频下载地址: https://pan.baidu.com/s/1i4Y9fbJ 密码: 7rkb
- svg入门详解
一.svg是什么? SVG 意为可缩放矢量图形(Scalable Vector Graphics). SVG 是使用 XML 来描述二维图形和绘图程序的语言. SVG 图像在放大或改变尺寸的情况下其图 ...
- Servlet使用反射机制
传统servlet存在的问题 每一个不同的请求都要写Servlet,导致整个项目servlet过多,不易维护 解决方案 同一个模块只写一个Servlet,然后每一个请求传一个参数,后台根据参数取调用不 ...
- 第10组 Beta冲刺(3/5)
链接部分 队名:女生都队 组长博客: 博客链接 作业博客:博客链接 小组内容 恩泽(组长) 过去两天完成了哪些任务 描述 新增修改用户信息.任务完成反馈等功能API 服务器后端部署,API接口的bet ...