上一节讲到java socket的服务端和客户端的简单通信,了解到socket的通信机制,详见:通信编程之java socket初探
今天我们继续深入一下,之前的例子有一个问题,就是只能发送一次消息就结束了,我们知道微信、QQ都是持续的收发消息的,那我们怎么才能使客户端持续的发送消息呢?下面我们就来实战探讨下。
socket的服务端是阻塞式的通信的,通过accept()方法来阻塞,等待客户端的连接,连接后客户端发送消息,通过IO来收发消息。从这个流程上来看,我们持续的执行这个动作,那么就能收到客户端的消息了,大家想到了,是的,服务端通过while(true)来控制循环收发消息。我们来看下服务端的代码:
package socketStudy;
import java.io.*;
import java.net.*;
/**
* socket 服务端
* @author xiaoming
* @version 1.0
* @date 2022-01-28
*/
public class CommunicationServer {
public static String socketserver_ip = "127.0.0.1";
public static int socketserver_port = 8881;
public static void main(String[] args) throws IOException {
startSocketForSimp();
}
/**
* 简单的socket服务端,可以连接一个客户的端,持续通信
*/
public static void startSocketForSimp(){
try {
ServerSocket ss = new ServerSocket(socketserver_port);
System.out.println("CommunicationServer启动服务器....端口为:"+socketserver_port+" wait connect...");
Socket s = ss.accept();
System.out.println("收到客户端连接,客户端:"+s.getInetAddress().getHostAddress()+"已连接到服务器");
//持续读取和发送消息
readAndWriteMsg(s.getInputStream(),s.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取和写入消息
* @param inp
* @param outp
* @throws IOException
*/
public static void readAndWriteMsg(InputStream inp,OutputStream outp) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(inp));
//持续读取客户端发送来的消息
while(true) {
String mess = br.readLine();
System.out.println("【收到客户端信息】信息为:" + mess);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
bw.write("【服务端】已收到客户端发送消息,消息为:"+mess+"\n");
bw.flush();
}
}
}
从以上代码可以看出,当一个客户端连接后,进入while(true)循环中,通过br.readLine()持续的读取客户端的消息,打印到控制台上。再看下客户端的代码:
package moreClientAndThread;
import socketStudy.CommunicationServer;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* socket客户端代码
* @author xiaoming
* @version 1.0
* @date 2022-02-05
*/
public class ClientSocket1 {
public static void main(String[] args) {
try {
//连接socket服务端
Socket s = new Socket(CommunicationServer.socketserver_ip,CommunicationServer.socketserver_port);
//构建IO
InputStream inp = s.getInputStream();//输入流,收到的信息
OutputStream outp = s.getOutputStream();//输出流,发出去的消息
//从控制台获取消息,向服务器端发送一条消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
while(true){
String str = bufferedReader.readLine();//从控制台读取消息
bw.write(str);
bw.write("\n");//表示一条信息结束了,服务端通过
bw.flush();
//读取服务器返回的消息
BufferedReader br = new BufferedReader(new InputStreamReader(inp));
String mess = br.readLine();
System.out.println("【收到服务器信息】:"+mess);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
从客户端代码中可以看到,通过从控制台上持续的接受输入的消息,发送到服务端,我们来看下运行的效果:
服务端运行结果:
收到客户端连接,客户端:127.0.0.1已连接到服务器
【收到客户端信息】信息为:1-123
【收到客户端信息】信息为:1-qwea
客户端运行结果:
1-123
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-123
1-qwea
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-qwea
从而实现了客户端持续的发送消息。但是上面的代码也有个问题,因为我们通过while阻塞在那里了,所以新的客户端连结过来的时候是连接不上的,从而只能连接一个客户端。如果我们要实现类似微信这样的通信,还需要实现多客户端同时发送消息。那么我们怎么实现多客户端通信呢?
如果要实现多个客户端同时连接并通信,那么我们有什么办法呢?我们分析一下服务端的处理流程,有两个核心点,一个是接受客户端的连接accept(),一个是连接上之后,持续的读取客户端你的消息,者两个地方是冲突的,所以我们需要将两者分离。分离的办法就是,接收到一个新的客户端连接之后,起一个新的线程来处理这个客户端的消息的的读取,和主线程分离,从而在服务端产生多线程,每一个客户端是一个独立的子线程。总结下这个方法,就是在服务端的主线程中处理客户端的连接,在子线程中处理客户端的消息读取。
我们来看下服务端的代码:
package socketStudy;
import java.io.*;
import java.net.*;
/**
* socket 服务端
* @author xiaoming
* @version 1.0
* @date 2022-01-28
*/
public class CommunicationServer {
public static String socketserver_ip = "127.0.0.1";
public static int socketserver_port = 8881;
public static void main(String[] args) throws IOException {
startSocketForMoreThread();
}
/**
* 多线程通信socket服务端
*/
public static void startSocketForMoreThread() throws IOException {
ServerSocket ss = new ServerSocket(socketserver_port);
System.out.println("CommunicationServer启动服务器....端口为:"+socketserver_port+" wait connect...");
while(true){
Socket s = ss.accept();
System.out.println("收到客户端连接,客户端:"+s.getInetAddress().getHostAddress()+"已连接到服务器");
//起一个线程处理
new Thread(new Runnable() {
@Override
public void run() {
try {
//读取和写入消息
readAndWriteMsg(s.getInputStream(),s.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 读取和写入消息
* @param inp
* @param outp
* @throws IOException
*/
public static void readAndWriteMsg(InputStream inp,OutputStream outp) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(inp));
//持续读取客户端发送来的消息
while(true) {
Thread t = Thread.currentThread();
String tname = t.getName();
String mess = br.readLine();
System.out.println("线程name="+tname+"【收到客户端信息】信息为:" + mess);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
bw.write("【服务端】已收到客户端发送消息,消息为:"+mess+"\n");
bw.flush();
}
}
}
从服务端代码可以看出,我们将accept()和消息的读写分离了,客户端消息读写单独起来一个线程来进行处理,我们特地将线程的name打印出来,来区分多线程处理的情况,从而更直观的看到多线程的处理过程。我们再看下客户端的代码,我们写了两个客户端类ClientSocket1:
package moreClientAndThread;
import socketStudy.CommunicationServer;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* socket客户端代码
* @author xiaoming
* @version 1.0
* @date 2022-02-05
*/
public class ClientSocket1 {
public static void main(String[] args) {
try {
//连接socket服务端
Socket s = new Socket(CommunicationServer.socketserver_ip,CommunicationServer.socketserver_port);
//构建IO
InputStream inp = s.getInputStream();//输入流,收到的信息
OutputStream outp = s.getOutputStream();//输出流,发出去的消息
//从控制台获取消息,向服务器端发送一条消息
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp));
while(true){
String str = bufferedReader.readLine();//从控制台读取消息
bw.write(str);
bw.write("\n");//表示一条信息结束了,服务端通过
bw.flush();
//读取服务器返回的消息
BufferedReader br = new BufferedReader(new InputStreamReader(inp));
String mess = br.readLine();
System.out.println("【收到服务器信息】:"+mess);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
再写一个客户端2的类,ClientSocket2,代码可以是一样的,ClientSocket2的代码我就不贴出来了。
我们先启动服务端代码,再依次启动客户端代码,我们看下实际运行的效果,如下:
服务端运行结果:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
CommunicationServer启动服务器....端口为:8881 wait connect...
收到客户端连接,客户端:127.0.0.1已连接到服务器
线程name=Thread-0【收到客户端信息】信息为:1-123
线程name=Thread-0【收到客户端信息】信息为:1-qwe
收到客户端连接,客户端:127.0.0.1已连接到服务器
线程name=Thread-1【收到客户端信息】信息为:2-qwe
线程name=Thread-1【收到客户端信息】信息为:2-qw1
客户端1的运行结果:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
1-123
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-123
1-qwe
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:1-qwe
客户端2的运行结果:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
2-qwe
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qwe
2-qw1
【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qw1
从以上可以看到我们实现了多客户端的连接,并通信。
在实际代码编写和调试的环节遇到了一些问题,做下记录分享。
多线程的使用,大家都学过,但是实际项目中并不一定使用过,时间长了也容易忘记,我在使用的时候,又复习了下多线程的用法,发现多线程的知识还是很深的,包扩进程和线程的关系,多线程的启动,线程池,线程间通信,线程处理的结果返回等等,这个后续我单独写一篇文章分享一下。
我本次使用的是new Thread(new Runnable(){})在Runnable里面重写了run()方法,运行后都正常,但是就是没有执行run()方法里面的消息收发逻辑,排查了一遍后发现,没有调用.start()方法,因为没有报错,排查起来有点绕,对于长期不写多线程的伙伴容易出现这个问题。正确用法要如下,不要忘记调用 .start()方法。
new Thread(new Runnable(){
@Override
public void run(){
//TODO:
}
}).start()
在写代码的过程中,由于多个逻辑切换,代码揉在一个方法中,导致每次改动都在一个方法中改动,容易出错,浪费时间,后来将整体流程分为客户端连接,消息的收发处理,然后再main函数中进行调用,这样每次改动的时候只影响一个方法里面的逻辑,从而大大减少了出错的次数,调试时间也大大减少,处理逻辑也更清晰了,代码量也少了很多。
还有将一些常量抽出来进行复用,也可减少代码量,减少出错的概率,养成好习惯。
这个问题比较复杂,网上很多资料都说不明白,我也没研究明白,后续继续研究,争取写一篇专栏探讨Connection reset问题及其解决办法。