Java套接字socket编程笔记
相对于C和C++来说,Java中的socket编程是比较简单的,比较多的细节都已经被封装好了,每次创建socket连接只需要知道地址和端口即可。
在了解socket编程之前,我们先来了解一下读写数据的数据流类中一些需要注意的东西。
BufferedReader与DataInputStream的区别:
通常我们常用到的字节输入输出流有BufferedReader与PrintWriter,DataInputStream和DataOutputStream这两对。这些类都属于java.io包。
那么两者之间有什么区别呢?
区别就是前者有个缓冲区,假如我们人为设置为100k(不设置亦可,有默认值),当这个缓冲区存储的内容达到100k的时候,类对象才会进行读入或写入操作。
而Stream的两个对象是没有缓冲区的,它们是收到什么数据就即刻进行读出和写入。
所以在进行socket编程的时候,这两对最好不要交替使用,因为当有数据存到前面提到的缓存里的时候,stream对象没有办法读到缓存里的东西,所以会造成数据的丢失。
在这里我们另外说一说PrintWriter类,先看看比较常用的两个构造方法:
在第二个构造方法中,参数2指明该对象是否自动将缓冲区里的数据流自动刷出,一般来说我们可以采用第二种构造方法,将参数2设为true。
否则,在每次用PrintWriter对象调用printXXX方法的时候,后面就要紧接着使用flush方法。
比如:
PrintWriter pw = new PrintWriter(socket.getOutputStream);
pw.println(“写出数据”);
pw.flush();
如果你不这么做的话,pw对象可能会因为你要写出的数据并未到达缓冲区指定大小而不作任何操作。这个时候你的线程就会阻塞!!所以关于这一点务必小心。
Java里的Socket工作模式:
在socket编程中我们基本上需要用到这些类:
SocketServer、Socket、BufferedReader与PrintWriter(或者DataInputStream与DataOutputStream)。
在服务器中,首先新建一个服务器socket对象:
ServerSocket srvSocket = new ServerSocket(nPort);
一旦接收到请求,则生成一个socket对象:
Socket socket = srvSocket.accept();
然后创建流对象:
BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter pw = new PrintWriter(socket.getOutputStream(),true);
客户端里直接进行连接然后创建流对象即可:
Socket socket = new Socket(hostAddr, nPort);
Socket编程基础Java实现:
其实在实现过程中遇到挺多很细节但是又很让人蛋疼的问题,比如前面提到的PrintWriter对象要么初始化的时候就设定为自动刷出缓存区内容,要么就每次写操作后面调用flush方法。下面给出实现方法:
客户端实现:
客户端实现的功能是这样的,输入一些特定的字符串,比如:DATE,BYE,DOY,DOM,DOW什么的,然后让服务器判断输入的是什么命令,然后服务器调用Calendar类返回对应的日期和时间信息。
我希望能从控制台读取用户输入的信息,所以设计了如下代码:
BufferedReader inSys = new BufferedReader(new InputStreamReader(System.in)); while((string = inSys.readLine()) != null && string.length() != 0) { System.out.println("客户端这边输入的命令是"+string); pw.println(string); System.out.println("服务器返还的数据是"+bf.readLine()); //ctrl+z or Enter to terminate the loop }
这样的话,但凡是用户按了ctrl+z或者是Enter键,则结束循环。
下面给出客户端完整实现代码:
import java.io.*; import java.net.*; import java.util.*; public class NeroSocketClient { public static void main(String[] args) throws IOException { // TODO Auto-generated method stub System.out.println("新客户端开启"); //三个变量要放在try-catch块前声明,不然的话在finall块中,try里面声明和定义的内容是不可见的,属于不同的作用域,生命周期不同 Socket socket = null; BufferedReader bf = null; PrintWriter pw = null; try { socket = new Socket("127.0.0.1", 8888); bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); pw = new PrintWriter(socket.getOutputStream(),true); //参数2:自动flush缓冲区内容 BufferedReader inSys = new BufferedReader(new InputStreamReader(System.in)); String string; System.out.println("支持的命令如下:"); System.out.println("BYE:结束连接"); System.out.println("DATE:日期和时间"); System.out.println("DOW:day_of_week"); System.out.println("DOM:day_of_month"); System.out.println("DOY:day_of_year"); System.out.println("PAUSE:暂停"); System.out.println("举个例子:客户端这边输入的是DATE"); pw.println("DATE"); pw.flush(); System.out.println ("服务器返还的数据是"+bf.readLine ()); System.out.println("请输入:"); while((string = inSys.readLine()) != null && string.length() != 0) { System.out.println("客户端这边输入的命令是"+string); pw.println(string); System.out.println("服务器返还的数据是"+bf.readLine()); //ctrl+z or Enter to terminate the loop } } catch (IOException e) { // TODO: handle exception System.out.println (e.toString ()); }finally{ //关闭连接 try { if (bf != null) { bf.close(); } if (pw != null) { pw.close(); } if (socket != null) { socket.close(); } } catch (IOException e2) { // TODO: handle exception } } } }
服务器实现:
设计服务器的时候,对进行数据读写操作的类应用Runnable接口,这样即可实现多线程,因为服务器没可能只对一个客户端提供服务的,所以写练习程序的时候直接写多线程的即可,从最基本的练起没必要,进度太慢。
在服务器的主方法里面,我们通过一个无限循环来不断地接受新发现的连接请求:
ServerSocket srvSocket = new ServerSocket(8888); while(true) //服务器是需要一直运行的,这样可以不断地监听和接收新的socket连接 { Socket socket = srvSocket.accept(); //收到新的请求 System.out.println("收到新的socket连接请求"); ServerThread sThread = new ServerThread(socket); Thread thread = new Thread(sThread); thread.start(); //上面的三行代码,不妨直接写成: //new Thread(new ServerThread(socket)).start(); }
这样的话,服务器即可一直运行。
具体操作数据的方法写在从接口继承来的run方法即可,这个方法是必须被重载的。
下面给出服务器实现代码:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.*; import java.util.*; public class NeroSocketServer { public static void main(String[] args) throws IOException{ // TODO Auto-generated method stub System.out.println("新服务器开启"); ServerSocket srvSocket = new ServerSocket(8888); while(true) //服务器是需要一直运行的,这样可以不断地监听和接收新的socket连接 { Socket socket = srvSocket.accept(); //收到新的请求 System.out.println("收到新的socket连接请求"); ServerThread sThread = new ServerThread(socket); Thread thread = new Thread(sThread); thread.start(); //上面的三行代码,不妨直接写成: //new Thread(new ServerThread(socket)).start(); } } } //应用这个接口,在run方法里面定义具体操作 class ServerThread implements Runnable { private Socket socket; //构造函数 public ServerThread(Socket s) { this.socket = s; } @Override public void run() { // TODO Auto-generated method stub System.out.println("线程开始"); BufferedReader bf = null; PrintWriter pw = null; try { bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); pw = new PrintWriter(socket.getOutputStream(),true); //如果参数2不设为true,则每次都需要执行flush操作 //程序实现功能:通过客户端请求,返回日期、时间等信息 //静态方法,直接调用 Calendar calendar = Calendar.getInstance(); System.out.println("准备进入循环"); while(true) //一直循环,直到用户请求完毕 { String s_request = bf.readLine(); //将所有命令转换为大写 s_request = s_request.toUpperCase(); System.out.println("当前接受到的命令是"+s_request); if (s_request.startsWith("BYE")) { //结束 break; } if (s_request.startsWith("DATE") || s_request.startsWith("TIME")) { System.out.println("输出日期和时间"); pw.println(calendar.getTime().toString()); System.out.println("输出完毕"); } if (s_request.startsWith("DOM")) { pw.println(""+calendar.get(Calendar.DAY_OF_MONTH)); //以字符形式写入 } if (s_request.startsWith("DOW")) { switch (calendar.get(Calendar.DAY_OF_WEEK)) { case Calendar.SUNDAY: pw.println("SUNDAY"); break; case Calendar.MONDAY: pw.println("MONDAY"); break; case Calendar.TUESDAY: pw.println("TUESDAY"); break; case Calendar.WEDNESDAY: pw.println("WEDNESDAY"); break; case Calendar.THURSDAY: pw.println("THURSDAY"); break; case Calendar.FRIDAY: pw.println("FRIDAY"); break; case Calendar.SATURDAY: pw.println("SATURDAY"); break; default: break; } } if (s_request.startsWith("DOY")) { pw.println(""+calendar.get(Calendar.DAY_OF_YEAR)); } if (s_request.startsWith("PAUSE")) { try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO: handle exception System.out.println(e.toString()); } } } } catch (Exception e) { // TODO: handle exception System.out.println(e.toString()); } finally{ //关闭连接 System.out.println("当前客户端断开连接"); try { if (bf != null) { bf.close(); } if (pw != null) { pw.close(); } if (socket != null) { socket.close(); } } catch (Exception e2) { // TODO: handle exception } } } }
最后我们看看客户端和服务器的运行情况,先运行服务器,然后运行客户端1,在客户端1输入一些测试命令以后,我们运行客户端2。
因为开启两个客户端是在同一个eclipse中开启,所以测试有点不准确,不过也懒得去开多个编译器了,就是那么一回事。对于这个问题呢,我们可以开多几个eclipse来运行(工作空间必须是不同的)。
也可以直接在控制台(cmd)里进行编译“javac 主类名.java”,生成.class字节码文件以后,用“java 类名”的方式运行客户端,在此不作演示。
客户端(左边)的截图只记录了第二个客户端开启以后输入的信息。