通信编程之java socket初探及持续通信和多线程通信
通信编程之java socket初探
最近在思考一些软件的实现原理,研究一下那些热门软件是怎么开发出来的,颇有点意思。回顾每天使用的软件,发现平时用的最多的软件应该是通信软件了,如微信、钉钉等,于是在想,这些通信软件是怎么开发出来的,使用了哪些技术?平时大家开发软件的工作大部分是CRUD,对一些不是自己的工作领域,一些底层技术研究的比较少,所以利用业余时间也对一些底层技术和自己感兴趣的软件做些研究。这篇是对通信软件的初探。
Java通信软件的开发技术从socket说起,下面对socket技术做些基础的了解和练习。
先看一下实战的效果图:
一、socket是什么?
网络通信里面,我们都听过TCP/UDP,HTTP,那么Socket是什么?Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。通常可用于通信软件开发,RPC协议开发,文件传输开发等。Socket使用中主要分为ServerSocket和Socket, ServerSocket类表示 Socket 服务器端,Socket 类表示 Socket 客户端。
二、socket的交互流程
socket的使用流程是什么样的呢?
两者之间的交互过程如下:
1.服务器端创建一个 ServerSocket(服务器端套接字),调用 accept() 方法等待客户端来连接。
2.客户端程序创建一个 Socket,请求与服务器建立连接。
3.服务器接收客户的连接请求,同时创建一个新的 Socket 与客户建立连接,服务器继续等待新的请求。
4.使用网络I/O来操作消息的发送和接收。如BufferedReader读/BufferedWriter写。
如下是socket的客户端和服务端的交互示意图。
三、实战演练
了解了socket的知识后,我们来进行一些实战练习。socket的通信实现分为服务端和客户端两个应用,在部署层面是两个独立的应用,我们先实现服务端的代码,如下:
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) { 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()+"已连接到服务器"); BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream())); //读取客户端发送来的消息 String mess = br.readLine(); System.out.println("【收到客户端信息】信息为:"+mess); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); bw.write("【服务端】已收到客户端发送消息,消息为:"+mess+"\n"); bw.flush(); } catch (IOException e) { e.printStackTrace(); } } }
首先,启动服务端socket程序,通过初始化ServerSocket(int port)实例就客户创建一个socket的服务端,port是服务端提供的端口号,对外提供的服务ip地址就是本机的IP地址,这里采用本地连接(127.0.0.1)。然后通过ServerSocket.accept()方法实现监听客户端的连接,之后就是使用BufferedReader、BufferedWriter来收发消息了。
下面在看看socket的客户端代码:
package socketStudy; import java.io.*; import java.net.*; /** * socket客户端 * @author xiaoming * @version 1.0 * @date 2022-01-28 */ public class CommunicationClient { 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();//输出流,发出去的消息 //向服务器端发送一条消息 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outp)); 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(); } } }
客户端首先使用new Socket(String ip,int port)来连接服务端的ip和端口号。然后使用BufferedReader、BufferedWriter来收发消息。先启动CommunicationServer.java,启动成功后,等待连接,会看到日志:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ...
CommunicationServer启动服务器....端口为:8881 wait connect...
这个时候服务端还没有任何客户端连接,再启动客户端程序,运行CommunicationClient.java,启动成功后,会看到服务端日志更新为如下:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ... CommunicationServer启动服务器....端口为:8881 wait connect... 收到客户端连接,客户端:127.0.0.1已连接到服务器 【收到客户端信息】信息为:测试客户端和服务器通信,服务器接收到消息返回到客户端
四、学习到的知识和问题总结1、学习到的知识
学到了socket的原理及初步用法,及实战中的容易出错点及纠正方法。
思考:怎么样实现持续的收发消息,及多客户端之间的通信呢?
2、遇到的问题
(1)、起类名字的冲突
自己新建了一个socket服务端类叫ServerSocket,结果和socket的服务端实现类冲突了,尴尬。。。
(2)、发送消息和收到的消息处理搞反了,这个很容易混淆。
好的方法是在参数定义上写清楚一些,并加上注释。
InputStream receiveMsgInputS //输入流,收到的信息
OutputStream sendMsgOutS //输出流,发出去的消息
(3)、程序运行的收发过程混乱
由于是服务端客户端模式,都会来回的收发消息,如果打印的日志不清晰的话,容易搞不清消息收发的过程,网上很多例子都能实现,但是探究其过程,对于初学者还是容易混淆的。建议在每个程序的入口和结尾出打印关键日志,日志中加上处理者的角色,处理的事情,及返回的结果,这样对与学习和排查问题都有很大的好处。
通信编程之java socket【二】-持续通信和多线程通信
今天我们继续深入一下,之前的例子有一个问题,就是只能发送一次消息就结束了,我们知道微信、QQ都是持续的收发消息的,那我们怎么才能使客户端持续的发送消息呢?下面我们就来实战探讨下。
一、java socket怎么持续通信
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阻塞在那里了,所以新的客户端连结过来的时候是连接不上的,从而只能连接一个客户端。如果我们要实现类似微信这样的通信,还需要实现多客户端同时发送消息。那么我们怎么实现多客户端通信呢?
二、java socket怎么多客户端通信如果要实现多个客户端同时连接并通信,那么我们有什么办法呢?我们分析一下服务端的处理流程,有两个核心点,一个是接受客户端的连接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
客户端2的运行结果:
"C:\Program Files\Java\jdk1.8.0_311\bin\java.exe" ... 2-qwe 【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qwe 2-qw1 【收到服务器信息】:【服务端】已收到客户端发送消息,消息为:2-qw1
从以上可以看到我们实现了多客户端的连接,并通信。
三、遇到的问题及解决办法
在实际代码编写和调试的环节遇到了一些问题,做下记录分享。
1、多线程的的使用
多线程的使用,大家都学过,但是实际项目中并不一定使用过,时间长了也容易忘记,我在使用的时候,又复习了下多线程的用法,发现多线程的知识还是很深的,包扩进程和线程的关系,多线程的启动,线程池,线程间通信,线程处理的结果返回等等,这个后续我单独写一篇文章分享一下。
我本次使用的是new Thread(new Runnable(){})在Runnable里面重写了run()方法,运行后都正常,但是就是没有执行run()方法里面的消息收发逻辑,排查了一遍后发现,没有调用.start()方法,因为没有报错,排查起来有点绕,对于长期不写多线程的伙伴容易出现这个问题。正确用法要如下,不要忘记调用 .start()方法。
new Thread(new Runnable(){ @Override public void run(){ //TODO: } }).start()
2、将代码整理成多个方法,抽取公共处理逻辑
在写代码的过程中,由于多个逻辑切换,代码揉在一个方法中,导致每次改动都在一个方法中改动,容易出错,浪费时间,后来将整体流程分为客户端连接,消息的收发处理,然后再main函数中进行调用,这样每次改动的时候只影响一个方法里面的逻辑,从而大大减少了出错的次数,调试时间也大大减少,处理逻辑也更清晰了,代码量也少了很多。
还有将一些常量抽出来进行复用,也可减少代码量,减少出错的概率,养成好习惯。
3、Connection reset问题
这个问题比较复杂,网上很多资料都说不明白,我也没研究明白,后续继续研究,争取写一篇专栏探讨Connection reset问题及其解决办法。
学问:纸上得来终觉浅,绝知此事要躬行
为事:工欲善其事,必先利其器。
态度:道阻且长,行则将至;行而不辍,未来可期
.....................................................................
------- 桃之夭夭,灼灼其华。之子于归,宜其室家。 ---------------
------- 桃之夭夭,有蕡其实。之子于归,宜其家室。 ---------------
------- 桃之夭夭,其叶蓁蓁。之子于归,宜其家人。 ---------------
=====================================================================
* 博客文章部分截图及内容来自于学习的书本及相应培训课程以及网络其他博客,仅做学习讨论之用,不做商业用途。
* 如有侵权,马上联系我,我立马删除对应链接。 * @author Alan -liu * @Email no008@foxmail.com
转载请标注出处! ✧*꧁一品堂.技术学习笔记꧂*✧. ---> https://www.cnblogs.com/ios9/