2009-01-28

简单的java网络程序

  由于封装掉底层的东西,所以要写一个简单的有网络通信功能的Java程序对于一个初学者来说还是比较简单的。
  首先我们需要了解一下Java的网络传输原理:
要使客户端能够工作,需要3个部分,当然,这不是tcp连接建立的3个过程。



  1. 客户端向服务器发起请求建立Socket连接
  2. 客户端传送信息给服务器
  3. 客户端从服务器接收信息

  我们从一个简单的聊天程序开始:先看程序,这个是客户端程序(我用绿色标明的是网络部分,蓝色标明的是多线程部分):
TalkBoxClient.java:

package talkbox;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class TalkBoxClient {
JTextArea incoming; //显示从服务器接收到的信息的文本域
JTextField outgoing; //输入要发送的信息的文本区
BufferedReader reader; //读取器缓冲
PrintWriter writer; //向服务器发送信息的文本发送器
Socket sock; //网络接口

public static void main(String[] args) {
TalkBoxClient client = new TalkBoxClient();
client.go();
}

public void go() {
/* ---------------------------GUI设置部分-----------------------------*/
JFrame frame = new JFrame("Simple talk box client");
JPanel mainPanel = new JPanel();
incoming = new JTextArea(15,50);
incoming.setLineWrap(true);
incoming.setWrapStyleWord(true);
incoming.setEditable(false);
JScrollPane qScroller = new JScrollPane(incoming);
qScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
qScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
outgoing = new JTextField(20);
JButton sendButton = new JButton("Send!");
sendButton.addActionListener(new SendButtonListener());
mainPanel.add(qScroller);
mainPanel.add(outgoing);
mainPanel.add(sendButton);
frame.getContentPane().add(BorderLayout.CENTER,mainPanel);
frame.setSize(400,500);
frame.setVisible(true);
/*---------------------------GUI配置结束------------------------------*/

setUpNetworking();

Thread readerThread = new Thread(new IncomingReader());
readerThread.start();

}

private void setUpNetworking() {
try {
sock = new Socket("127.0.0.1", 5000);
InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader(streamReader);
writer= new PrintWriter(sock.getOutputStream());
} catch(IOException e) {
e.printStackTrace();
}
}


public class IncomingReader implements Runnable {
public void run() {
String message;
try {
while ((message = reader.readLine()) != null) {
System.out.println("read " + message);
incoming.append(message + "\n");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}


public class SendButtonListener implements ActionListener{
public void actionPerformed(ActionEvent ae) {
try {
writer.println(outgoing.getText());
writer.flush();
} catch(Exception e) {
e.printStackTrace();
}
outgoing.setText("");
outgoing.requestFocus();
}
}

}

先来分析下客户端程序,你现在可以只需要看绿色的部分:
显然,绿色的部分:
程序直接调用了setUpNetwork方法,而方法的定义:

sock = new Socket("127.0.0.1", 5000);
  这一句对Socket对象的引用sock调用构造函数使其指向一个新产生的Socket对象,这个Socket对象建立了本机与127.0.0.1:5000之间的网络连接。换句话说,其实在Java中,构造了一个Socket对象,JVM会自动处理它和服务器直接tcp连接的问题,我们无需关心,使用的时候只要当作一个普通的流对象来处理即可。 这样就实现了同服务器建立连接。

InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());
  InputStreamReader是用来从Socket获得流的方法,我们新建了一个InputStreamReader对象来从Socket对象的getInputStream方法处获得流。这里方法的名字我们可以理解为都是相对于我们这个客户端程序来说,所以从服务器获得的流是InputStream。

reader = new BufferedReader(streamReader);
  reader是一个BufferedReader对象,就是一个缓冲型的读取器,一般读取写入数据的时候都会使用一个缓冲区来处理的。BufferedReader对应的写入器就是BufferedWriter了。

writer= new PrintWriter(sock.getOutputStream());
  这里用PrintWriter而不是用BufferedWriter的原因在于每次都是写入字符串,而PrintWriter能够提供方便的字符串写入方法。

  这样,通过reader和writer我们就成功的通过sock建立的连接来进行客户端和服务器的数据互相传输了。
  再看看SendButtonListener 这个实现了监听器接口的内部类。每当send按钮被点击时,客户端就会通过writer的方法
writer.println(outgoing.getText());
writer.flush();

向服务器立即写入一条数据。这样就实现了向服务器发送数据和获得数据了。

接下来看看服务端程序
TalkBoxServer.java:

package talkbox;
import java.io.*;
import java.net.*;
import java.util.*;

public class TalkBoxServer {
ArrayList clientOutputStreams;

public class ClientHandler implements Runnable {
BufferedReader reader;
Socket sock;

public ClientHandler(Socket clientSocket) {
try {
sock = clientSocket;
InputStreamReader isReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader(isReader);
} catch(Exception e) {
e.printStackTrace();
}
}

public void run() {
String message;
try {
while((message = reader.readLine()) != null) {
System.out.println("read " + message);
tellEveryone(message);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
new TalkBoxServer().go();
}

public void go() {
clientOutputStreams = new ArrayList();
try {
ServerSocket serverSock = new ServerSocket(5000);

while(true) {
Socket clientSocket = serverSock.accept();
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
clientOutputStreams.add(writer);


Thread t = new Thread(new ClientHandler(clientSocket));
t.start();

System.out.println("got a connection");
}
} catch(Exception e) {
e.printStackTrace();
}
}

public void tellEveryone(String message) {
Iterator it = clientOutputStreams.iterator();
while(it.hasNext()) {
try {
PrintWriter writer= (PrintWriter) it.next();
writer.println(message);
writer.flush();
} catch(Exception e) {
e.printStackTrace();
}
}
}
}


对于服务端来说,类似,我们也先只看绿色部分:

ServerSocket serverSock = new ServerSocket(5000);

  这句代码在本机的5000端口开启了一个服务端套接字对象,也就是监听了本机的5000端口用以应对客户的请求。

Socket clientSocket = serverSock.accept();

  在无限循环之中的这一句,调用了serverSock.accept()这个方法就等待连接。一旦有客户端连接了之前监听的端口的时候,这个方法就会返回一个新的Socket对象,返回的这个新的对象会建立服务器和客户端直接的套接字连接,但是服务器端的socket端口号被转成另外一个,以便于原来的端口能够继续监听等待客户端的请求,不过我们通过netstat命令看到的还是连接到5000的连接。由于这个方法在没有接收到连接之前会阻塞,也就是会让程序一直停在这里直到有客户端连接上,因此程序就不会无限的创建socket对象了。。。

  当然,这个只是为说明基本原理而存在的程序,我按照Head First Java里面的范例几乎没有修改的打过来的。这个程序几乎没有健壮性可言,但是用来说明问题已经足够。它是可以运行的。你可以试试。

  至于多线程部分,实际上客户端用多线程是为了能够同时向服务器发送信息和接收信息(你不会希望你的聊天程序在你不发信息的情况下收不到服务器的信息吧),因此while的条件

message = reader.readLine()

  这一句会一直等待套接字连接从服务器接收到信息。只要socket不挂它就能一直循环。

  而服务器端则是为了对每一个客户端建立一个Socket实例来向客户端读写信息。

没有评论: