用java实现socket C/S通信很简单,很多教科书上都有。但是这些通信模型大都是阻塞式的,其弊端也很明显:一方必须要接收的到对方的消息后,才能编辑自己的消息发出。同样对方也要一直等待这条消息收到后才能发送新的消息。用网络通信的知识讲,大概就是半双工通信吧。这就好比聊天的时候,两个人只能一人一句的聊天。不能一个人连着发送多句话。
而要实现非阻塞通信呢,也就是实现全双工通信。我不想使用java的NIO包。因为那样有点小题大做了。其实只要使用多线程就能实现了。
Socket和ServerSocket
//服务端:
ServerSocket ss = new ServerSocket(5432);
//客户端:
Socket s = new Socket("127.0.0.1",5432);
//服务端:
Socket s = ss.accept();
注意以上的代码要捕获相应的异常。这就不多说了,eclipse会帮助我们。
IO操作
socket的IO流
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
由此获得的两个字节IO流就是接下来所有数据交互的基础了。然后为了提高效率要进行一下包装。我是用的是DataInputStream/DataOutputStream。你也可以转换为字符流。
DataInputStream din = new DataInputStream(in);
DataOutputStream dout = new DataOutputStream(out);
很多人可能会把输入流和输出流搞混淆掉哦。
标准输入的包装
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() |
接着我对控制台的标准输入的包装使用了BufferedReader(也可以使用Scanner包装哦)。
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
String msg = bf.readLine();
dout.writeUTF(msg);
多线程的使用
- 一个是从socket的输入流中获取对方的消息并打印在屏幕上。
- 从标准输入中键入的消息要通过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盘传给了旁边的同学,最后两台电脑实现了通信,呵呵。虽然是小事,还是蛮骄傲的呢。
————————————————————————————————————————————