/**PageBeginHtml Block Begin **/ /***自定义返回顶部小火箭***/ /*生成博客目录的JS 开始*/ /*生成博客目录的JS 结束*/

通信编程之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问题及其解决办法。
 

 

posted @ 2023-02-02 09:58  一品堂.技术学习笔记  阅读(157)  评论(0编辑  收藏  举报