上位机与下位机交互--让socket不"死"

需求说明:

下位机是plc,西门子1200

下位机只能做服务器端,监听一个端口,不能主动给客户端发送消息(原计划是上位机也是监听一个端口,供下位机来访问,上传数据,结果现实很骨感)

上位机(pc)充当客户端,可以主动连接下位机交换信息

具体需求:

1,上位机给下位机下达工作数据

(比如下位机是生产纸张,上位机需要发给下位机纸张的尺寸,数量等数据)

2,下位机会在某个特定的时间“发送”数据,供上位机“接收”

(注意  “发送” 是加上引号的,因为服务器(下位机)是不能主动给客户端(上位机,pc)发送数据的,这是此文需要解决的问题

需求1毫无难度,直接在用户在上位机点击下单,上位机即可创建一个socket将相关数据下传即可

重点来看看需求2;

可以想象为服务器与客户端(浏览器)

不妨以秒杀为例,

客户端(浏览器)提交秒杀请求(相当于这里的上位机给下位机下单),由于秒杀并发很大,不能实时返回数据(一般是异步下单,这里不深究)

客户端如果需要知道秒杀的结果,会在浏览器(客户端)通过轮询查询订单状态(如果有,则表示下单成功)

在需求2中,“客户端” 就指的是上位机创建的“socket”,socket是唯一和下位机沟通的途径,而且下位机不能创建socket,主动发数据给上位机,那么上位机创建得到socket,能否让他成为一个永久的客户端,上位机定时的取这个socket里的数据呢??

答案是肯定的:

思路:

将socket封装成一个单例,需求1和需求2中的客户端(上位机的socket) 使用同一个socket

问题又来了:

问题1:

如果上位机正在给下位机下传工作信息,下位机恰好也在此时完成了一些工作,将工作状态通过socket(由上位机创建的)发送数据,那么会有可能出现冲突,有点乱套

解决:

尝试加锁,让这个socket同一时间只能用在  过程1 ( 上位机给下位机下传工作数据)

                                                                           或者 过程2(上位机周期读取socket中的数据(也就是下位机返回的工作完成情况这些数据)),

通过加锁:让这一个socket在某一个时刻,只能用于过程1或者过程2

问题2:

如果socket挂掉了?该如何获取与下位机的交互数据的“要道” 呢??

解决:

封装后的socket单例类,在使用socket读、写时,加上try catch 捕捉  SocketException,出现此异常,socket连接就是断开了,重新是用封装好的单例类的getInstance方法获取 socket对象,重建连接

此单例实现代码如下:

其中需要注意的是:

1,Semphore这一锁(信号量),参数指定此锁允许最多同时被多少个线程使用某一个资源(或者是资源的个数)

2,isDead 标记,用来标记当前单例对象是否断开(socket),如果是断开的,在getInstance是对其做了判断

如果没有单例对象,或者单例对象标记为“死亡”,那么就创建新的单例对象

第2点保证了,同一时刻,只能有一个线程使用socket

第2点保证了,socket的持久通信,及时下位机重启或者其他原因导致socket连接中断,也能重新创建连接


/**
 * socket单例  用于维系上位机与下位机的通信
 * 需要保证是同一个socket,并且需要注意锁的问题
 */
public class SocketSingleton extends Socket{
    //只允同时一个线程访问此单例
    public static Semaphore singletonLock=new Semaphore(1);
    private boolean isLocked;//标记单例是否被锁住,有Semaphore,这里就可以省略了
    private volatile  boolean isDead;//标记单例是否死亡
    private static SocketSingleton ourInstance=null;

    private SocketSingleton() throws IOException {
            super(WINDER_IP,WINDER_PORT);
             isDead=false;
            ourInstance=this;
            isLocked=false;
    }

    /**
     * 获取锁
     */
   public synchronized  void  getLock(){
        this.isLocked=true;
   }

    /**
     * 释放锁
     */
   public synchronized  void  releaseLock(){
        this.isLocked=false;
   }

    /**
     *
     * @return 检测锁
     */
    public synchronized  boolean checkIfLock(){
        return this.isLocked;
    }

    /**
     *   kill 连接异常的socket
     */

    public static void killSingleton(){
        ourInstance.isDead=true;
    }
    /**
     *
     * @return
     * @throws IOException
     * 如果当前单例的socket是dead,就创建新的socket
     */
    public synchronized static SocketSingleton getInstance() throws IOException {
        return (Objects.isNull(ourInstance)||ourInstance.isDead) ? new SocketSingleton():ourInstance;
  }
}

使用:

以下位机状态“返回“”为例”

1 先获取锁(获取资源)

2  业务操作

3 再释放锁(释放资源)

 @Scheduled(cron = "0/10 * * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void myTestWork() throws IOException, InterruptedException {

        TubeServiceWithPC2Winder tubeServiceWithPC2Winder = new TubeServiceWithPC2Winder();
        log.info("定时器启动!{}", new Timestamp(System.currentTimeMillis()));
        SocketSingleton socket=SocketSingleton.getInstance();

       try{
//理解为获取锁,或者是获取资源,如果没有,这里会阻塞吗,直到其他线程将资源释放,这里才会往下进行
           SocketSingleton.singletonLock.acquire();
        log.info("状态交互获取到锁  singletonLock:{}",new Timestamp(System.currentTimeMillis()));

 
           socket.setSoTimeout( SOCKET_TIME_READ_timeout);
           log.info("状态交互 hashcode:{}",socket.hashCode());
           InputStream inputStream = socket.getInputStream();
           OutputStream outputStream = socket.getOutputStream();
           byte[] dataFromServer = new byte[WORK_FINISHED_BYTES_LENGTH];
           inputStream.read(dataFromServer);
           ByteQueue byteQueue = new ByteQueue(dataFromServer);
           log.info("来自服务端的数据:{}", byteQueue.toString());
   //数据处理的业务逻辑,此处忽略
           Tube tubeInfo = resolveData(dataFromServer);
         


       }catch(SocketTimeoutException e){

           log.info("状态交互 read 超时");
       }catch (SocketException e){

          //处理连接中断,重新创建单例socket
           SocketSingleton.killSingleton();
             SocketSingleton.getInstance();
       }  finally{
// 这里理解为释放锁,或者是释放资源(一共只有1个资源),如果不释放,其他线程将不能获得此资源,比如下一个周期到了,定时器将停在     SocketSingleton.singletonLock.acquire() 无法向下进行
           SocketSingleton.singletonLock.release();
           log.info("状态交互 释放成功  singletonLock:{}",new   Timestamp(System.currentTimeMillis()));
       }



    }

定时任务改进说明:

最初是在定时器里面没有使用单例,每个周期到了,将new Socket(),发现随着执行的任务次数增加,会出现问题

内存资源的问题:

比如我创建2000个socket:

 @Test
    public  void  testCreateMoreSocket() throws IOException, InterruptedException {
       for(int i=0;i<2000;i++){
           Socket socket=new Socket(WINDER_IP,WINDER_PORT);
           log.info("socket:{},hashCode:{}",new Object[]{i,socket.hashCode()});
//           Thread.sleep(1*1000);
       }
    }

创建之前:

上位机与下位机交互--让socket不

创建之后:变化感觉不大,但是idea电脑卡死将近5秒

上位机与下位机交互--让socket不

定时任务是会一直执行的,如果是4000次又会是怎样的呢?

当创建完1563个的时候已经抛异常了,有一瞬间cpu使用率100%

改下代码,创建socket使用完就将其close,测试结果竟然让调试助手还是无响应了,

  @Test
    public  void  testCreateMoreSocket() throws IOException, InterruptedException {
       for(int i=0;i<4000;i++){
           Socket socket=new Socket(WINDER_IP,WINDER_PORT);
           log.info("socket:{},hashCode:{}",new Object[]{i,socket.hashCode()});
//           Thread.sleep(1*1000);
           socket.close();
       }
    }

上位机与下位机交互--让socket不

可能是创建台频繁,那就延时1秒试试,然后喝 1*n 杯咖啡(4000个创建完需要一个小时,暂时就不测试了,毕竟咖啡不够了)

上面的测试过程是为了说明创建socket是需要耗费资源,那么就得减少socket的创建,完成任务就好

 

你可能感兴趣的:(web后端,多线程,设计模式)