• Java简单实现Socket非阻塞通信


            用java实现socket C/S通信很简单,很多教科书上都有。但是这些通信模型大都是阻塞式的,其弊端也很明显:一方必须要接收的到对方的消息后,才能编辑自己的消息发出。同样对方也要一直等待这条消息收到后才能发送新的消息。用网络通信的知识讲,大概就是半双工通信吧。这就好比聊天的时候,两个人只能一人一句的聊天。不能一个人连着发送多句话。

            而要实现非阻塞通信呢,也就是实现全双工通信。我不想使用java的NIO包。因为那样有点小题大做了。其实只要使用多线程就能实现了。

    Socket和ServerSocket

        
        这个就不多说了,默认大家都懂了。顾名思义,ServerSocket是服务器(S)端使用的。Socket是客户端(C)使用的。也就是所谓的C/S。
        ServerSocket的构造方法要指定程序使用的端口(Port)。从网络通信的角度来看,要通信的话,必须要指定一个端口,因为在应用层中的程序太多了。这样才能把数据发送给对应的应用程序。端口号的值要小于65535,并且大于1023。而客户端除了要指定端口以外还要指定服务器的IP地址。在本机上测试的话可以使用“回环地址(localhost)”127.0.0.1。
    //服务端:
    ServerSocket ss = new ServerSocket(5432);
    //客户端:
    Socket s = new Socket("127.0.0.1",5432);

    ————————————————————————————————————————————
      端口号要小于65535的原因是,通信过程都是通过IP数据报完成的。IP数据报的报头中包含一个16位字段用来指定端口号。而2的16次方就是65536。所以其范围应该是0~65535
      端口号之所以要大于1023是因为小于1023的都是知名端口号,也就是已经确定了某些特殊用途的端口号。比如,FTP服务器的TCP端口号是21,Telnet是23,Http的端口是80。
    ————————————————————————————————————————————
        但是要实现通信双方都必须通过Socket来通信。服务端可以通过ServerSocket的accept方法来获得一个Socket对象。
    //服务端:
    Socket s = ss.accept();
    
       注意以上的代码要捕获相应的异常。这就不多说了,eclipse会帮助我们。
        此时呢,服务端和客户端的类中都持有了一个Socket对象。要完成数据交换,就要涉及IO了。
     

    IO操作


    socket的IO流

        Socket有相应的get方法来获得输入输出流。
    	InputStream in = s.getInputStream();
    	OutputStream out = s.getOutputStream();
        由此获得的两个字节IO流就是接下来所有数据交互的基础了。然后为了提高效率要进行一下包装。我是用的是DataInputStream/DataOutputStream。你也可以转换为字符流。
    	DataInputStream din = new DataInputStream(in);
    	DataOutputStream dout = new DataOutputStream(out);

    很多人可能会把输入流和输出流搞混淆掉哦。


    标准输入的包装

        我们要实现从控制台中输入,然后发送到网络的输入流(上文中的din),必须要对标准输入(System.in)进行封装。
    误区:
        起初我觉得既然从标准输入获得的数据,要传递给DataOutputStream类型的对象dout发送出去,那么就用DataInputStream来包装System.in吧。然后调用它的readUTF方法获得字符串,在通过dout的writeUTF方法发送。
    	DataInputStream dis = new DataInputStream(System.in));
    	String msg = dis.readUTF();
    	dout.writeUTF(msg);
        但是当程序运行的时候,就会发现在一方的控制台窗口无论输入多少行,对方都无法收到。也就是这里也陷入了阻塞,百度下,究其原因呢,是因为在控制台输入的字符并不是UTF格式的。所以readUTF根本无法返回字符串。
    ————————————————————————————————————————————

       DataInputStream/DataOutputStream提供了对于基本数据类型的写入与读取方法如:

     DataInputStream  DataOutputStream
    readInt() writeInt()
    readChar() writeChar()
    readDouble() writeDouble()
        所以输入流输出流中的数据类型必须相同才能读取。但是其中并没有readString/wirteString类型。只有一个readUTF/writeUTF(原来有readLine现在不鼓励了,但是BufferedReader类中使用的是readLine)。

    ————————————————————————————————————————————
        接着我对控制台的标准输入的包装使用了BufferedReader(也可以使用Scanner包装哦)。
    	BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
    	String msg = bf.readLine();
    	dout.writeUTF(msg);
    【下面进入本片博客的重要环节(上面的内容你都可以认为是凑字数的铺垫)】

    多线程的使用


    ————————————————————————————————————————————
         一般我们运行的程序只有一个主线程,也就是main函数创建的那个线程。一个线程中所有的操作呢都是线性的,但如果你想同时做多件事的话,就需要创建多个线程。记得《射雕》里面老顽童有个绝技“左右互博术”,他在传授给郭靖的时候,就是让郭靖不停的左手画圆,右手画方。这就相当于郭靖要同时开两个线程,一个线程负责不停地左手画圆,另一线程负责不停的右手画方。当然不论是电脑还是人脑实际上都是无法同时做两件事的,只不过CPU在两个线程之间快速切换,给人的感觉像是同时一样。
        Java本身提供了对于多线程的支持。通过继承Thread类,或者实现Runnable接口来创建新的线程。两者都要重写run()方法。不过采用Runnable,只是实现了Runnable接口的run方法还不够,还要用此实现的接口来创建Thread对象才可以。最后调用Thread对象的start()方法就是创建这一进程了。
    ————————————————————————————————————————————
     
        实现非阻塞的通信,我们要完成的事情只有两个:
    1. 一个是从socket的输入流中获取对方的消息并打印在屏幕上。
    2. 从标准输入中键入的消息要通过socket的输出流发送给对方。

        以上行为要开辟新的线程来完成来避免阻塞。为此我定义两个类,取名为SendThread和PrintThread

    为了便于操作,我将socket的包装后的输出流作为参数传递给SendThread。同样PrintThread中也会传如socket的包装后的输入流参数

    import java.io.BufferedReader;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class SendThread implements Runnable {
    
    	private DataOutputStream dout;
    	public SendThread(DataOutputStream dout){
    		super();
    		this.dout= dout;
    	}
    	@Override
    	public void run() {
    		// TODO Auto-generated method stub
    		while(true){
    			String msg;
    			try {
    				BufferedReader bf = new BufferedReader(
    						new InputStreamReader(System.in));
    				//注意不能直接用DataInputStream来封装标准输入,原因前文已提到
    				msg = bf.readLine();
    				dout.writeUTF(msg);
    			} catch (IOException e) {
    				// TODO Auto-generated catch block
    				
    				break;
    			}
    		}
    	}
    
    }
    

        为了增强可读性,我也把IP地址传入了PrintThread中。大家也可无视

    import java.io.DataInputStream;
    import java.io.IOException;
    
    class PrintThread implements Runnable{
    
    	private DataInputStream din;
    	private String ip;
    	public PrintThread(DataInputStream din,String ip) {
    		super();
    		this.din = din;
    		this.ip = ip;
    	}
    
    	@Override
    	public void run() {
    		// TODO Auto-generated method stub
    		while(true){
    			try {
    				String msg = din.readUTF();
    				System.out.println("["+ip+"]"+":"+msg);
    			} catch (IOException e) {
    				// TODO Auto-generated catch block
    				
    			break;
    			} 
    			
    		}
    	}
    	
    }

        接着在主类Server和Client中要做的事就很简单了。

    //Server
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server {
    
    	private static String ip;
    
    	public static void main(String[] args) throws IOException {
    		// TODO Auto-generated method stub
    		@SuppressWarnings("resource")
    		ServerSocket ss = new ServerSocket(5432);
    
    		Socket s = ss.accept();
    		ip = s.getInetAddress().toString();
    		ip = ip.substring(ip.indexOf("/")+1);
    		System.out.println(ip+"上线了");
    		
    		InputStream in = s.getInputStream();
    		OutputStream out = s.getOutputStream();
    		
    		DataInputStream din = new DataInputStream(in);
    		DataOutputStream dout = new DataOutputStream(out);
    		
    		SendThread it = new SendThread(dout);
    		PrintThread ot = new PrintThread(din,ip);
    		
    		new Thread(ot).start();
    		new Thread(it).start();
    	}
    }
    


     

    //Client
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    
    public class Client {
    
    	private static String ip = "127.0.0.1";
    	
    	public static void main(String[] args) throws IOException{
    		@SuppressWarnings("resource")
    		Socket s = new Socket(ip,5432);
    		OutputStream out = s.getOutputStream();
    		InputStream in = s.getInputStream();
    		
    		DataOutputStream dout = new DataOutputStream(out);
    		DataInputStream din = new DataInputStream(in);
    		
    		SendThread it = new SendThread(dout);
    		PrintThread ot = new PrintThread(din,ip);
    		
    		new Thread(ot).start();
    		new Thread(it).start();
    
    	}
    }


         基本的代码就是这些,当然这是简单实现,还可以有很多继续完善的地方。比如使Server可以监听多个Client的连接请求,或者你可以加个Swing的界面(个人比较不在乎界面)。细节我也有很多未仔细处理的地方,比如里面的Socket和各种IO流,都没有写关闭,(囧,没有找到个合适的地方)所以代码中会有一句

    @SuppressWarnings("resource")

    来忽略流未关闭的警告。。哈,有点水啊。


    ————————————————————后记的琐事———————————————————

        这段程序,我成功在电脑和虚拟机上实现了通信。虚拟机的原理就是把你的一台电脑当成两台电脑来用了。只需要修改上述代码中的IP地址就行了,虚拟机相当于和你处在一个局域网里,你可以使用ipconfig命令查看在这个局域网之下的虚拟机的ip地址,通常host-only的就是虚拟机的ip了。比如在这个虚拟的局域网中我本身的电脑IP地址是192.168.56.101,而虚拟机是192.168.56.1。

        还有就是那天数据结构上机课的时候,我发现机房电脑里装了java(但是没eclipse)。。于是我就用记事本手写了上面两个程序,果然用惯了IDE,记事本会很不习惯啊。但最后还是完成了,我把Client的程序通过U盘传给了旁边的同学,最后两台电脑实现了通信,呵呵。虽然是小事,还是蛮骄傲的呢。
    ————————————————————————————————————————————

  • 相关阅读:
    实验5 数独游戏界面设计
    实验4 颜色、字符串资源的使用 实验报告
    实验五 操作系统之存储管理
    实验四 主存空间的分配和回收
    实验三 进程调度模拟程序
    实验二作业调度模拟程序(先来先服务(FCFS)调度算法)
    实验八 SQLite数据库操作
    实验七 BindService模拟通信
    实验六 在应用程序中播放音频和视频
    实验五 数独游戏界面设计
  • 原文地址:https://www.cnblogs.com/unclejelly/p/4082095.html
Copyright © 2020-2023  润新知