1.使用ServerSocket创建TCP服务器端
Java中能接收其他通信实体连接请求的类是ServerSocket, ServerSocket对象用于监听来 自客户端的Socket连接,如果没有连接,它将一直处于等待状态。ServerSocket包含一个监听来自客户端连接请求的方法。
1) Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与连接客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。
创建ServerSocket对象,ServerSocket类提供了如下几个构造器:
2) ServerSocket(int port):用指定的端口 port 来创建一个ServerSocket。该端口应该是有一个有效的端口整数值:0?65 535。
3) ServerSocket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog。
4) ServerSocket(int port.int backlog,lnetAddress localAdd():在机器存在多个 IP 地 址的情况下,允许通过localAddr这个参数来指定将ServerSocket绑定到指定的IP地址。
注:当ServerSocket使用完毕后,应使用ServerSocket的close()方法来关闭该ServerSocket。通常情况下,服务器不应该只接收一个客户端请求,而应该不断地接收来自客户端的所有请求。如下面代码所示:
//创建一个ServerSocket,用于监听客户端的连接请求 ServerSocket ss=new ServerSocket(1566); //不停地从接收来自客户端的请求 while (true) { //每当接受一个来自客户端的Socket的请求,服务器端也对应产生一个Socket Socket s=ss.accept(); //下面就可以使用Socket进行通信了 //.......... } |
2.使用Socket进行通信
客户端通常可使用Socket的构造器来连接到指定服务器,Socket通常可使用如下两个构造器。
1) Socket(lnetAddress/String remoteAddress, int port):创建连接到指定远程主机、远程端口的Socket,该构造器没有指定本地地址、本地端口,默认使用本地主机的默认IP地址,默认使用系统动态指定的IP地址。
2) Socket(lnetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort):创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址 和本地端口号,适用于本地主机有多个IP地址的情形。
上面两个构造器中指定远程主机时既可使用InetAddress来指定,也可直接使用String对象来指定,但程序通常使用String对象(如211.158.6.26)来指定远程IP。当本地主机只有—个IP地址时,使用第一个方法更为简单。如:
Socket socket=new Socket("169.254.77.36", 8888); //下面就可以和服务器进行通信了 |
当程序执行上面代码中的粗体字代码时,该代码将会连接到指定服务器,让服务器端的ServerSocket的accept()方法向下执行,于是服务器端和客户端就产生一对互相连接的Socket。
当客户端、服务器端产生了对应的Socket之后,程序无须再区分服务器、客户端,而是通过各自的Socket进行通信。Socket提供如下两个方法来获取输入流和输出流:
1) InputStream getlnputStream():返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
2) OutputStream getOutputStream():返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。
3.实例:和服务器进行简单通信:
服务器端:
public static void main(String[] args) { // TODO Auto-generated method stub try { //创建一个ServerSocket,用于监听客户端的连接请求 ServerSocket ss=new ServerSocket(8888); //不停地从接收来自客户端的请求 while (true) { //每当接受一个来自客户端的Socket的请求,服务器端也对应产生一个Socket Socket s=ss.accept(); //下面就可以使用Socket进行通信了 OutputStream os=s.getOutputStream(); os.write("来自服务器端的消息:你好,今天天气不错,骚年外出散散心吧!".getBytes("utf-8")); //关闭输出流 os.close(); //关闭Socket s.close();
} } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } |
注:上面的程序并未把OutputStream流包装成PrintStream ,然后使用 PrintStream直接输出整个字符串,这是因为该服务器端程序运行于Windows 主机上,当直接使用PrintStream输出字符串时默认使用系统平台的字符串(即 GBK )进行编码;但该程序的客户端是Android应用,运行于Linux平台(Android 是Linux内核的),因此当客户端读取网络数据时默认使用UTF-8字符集进行解码,这样势必引起乱码。为了保证客户端能正常解析到数据,此处手动控制字符串的编码,强行指定使用UTF-8字符集进行编码,这样就可以避免乱码问
客户端:
edtMsg=(EditText)findViewById(R.id.edtMsg); //创建并启动一个新线程,向服务器发送TCP请求 new Thread(){ @Override public void run() { // TODO Auto-generated method stub super.run(); //创建一个Socket用于向IP为169.254.77.36的服务器的8888端口发送请求 Socket s; try { s = new Socket(); //如果超过10s还没连接到服务器则视为超时 s.connect(new InetSocketAddress("169.254.77.36", 8888),10000); //设置客户端与服务器建立连接的超时时长为30秒 s.setSoTimeout(30000); //将Socket对应的输入流封装成BufferedReader对象 BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream())); String msg=br.readLine(); edtMsg.setText(msg); br.close(); s.close(); //捕捉SocketTimeoutException异常 }catch (SocketTimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); }catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }.start(); |
最后别忘记为程序添加访问网络的权限:
<uses-permission android:name="android.permission.INTERNET"/> |
程序运行效果图:
4.异常和捕捉
上面的程序为了突出通过ServerSocket和Socket建立连接并通过底层 IO流进行通信的主题,程序没有进行异常处理,也没有使用finally块来关闭资源。
实际应用中,程序可能不想让执行网络连接、读取服务器数据的进程一直阻塞,而是希 望当网络连接、读取操作超过合理时间之后,系统自动认为该操作失败,这个合理时间就是 超时时长。Socket对象提供了一个setSoTimeout(int timeout)来设置超时时长,如下面的代码 片段所示:
//设置客户端与服务器建立连接的超时时长为30秒 s.setSoTimeout(30000); |
为Socket对象指定了超时时长之后,如果使用Socket进行读、写操作完成之前已经超出了该时间限制,那么这些方法就会抛出SocketTimeoutException异常,程序可以对该异常进行捕捉,并进行适当处理,如以下代码所示:
Socket s; try { s = new Socket(); //如果超过10s还没连接到服务器则视为超时 s.connect(new InetSocketAddress("169.254.77.36", 8888),10000); //设置客户端与服务器建立连接的超时时长为30秒 s.setSoTimeout(30000); //将Socket对应的输入流封装成BufferedReader对象 BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream())); String msg=br.readLine(); edtMsg.setText(msg); br.close(); s.close(); //捕捉SocketTimeoutException异常 }catch (SocketTimeoutException e) { //进行异常处理 } |
假设程序需要为Socket连接服务器时指定超时时长:即经过指定时间后,如果该Socket 还未连接到远程服务器,则系统认为该Socket连接超时。但Socket的所有构造器里都没有提供指定超时时长的参数,所以程序应该先创建一个无连接Socket,再调用Socket的connect() 方法来连接远程服务器,connect()方法就可以接受一个超时时长参数。如以下代码所示:
//创建一个无连接的Socket Socket s= new Socket(); //如果超过10s还没连接到服务器则视为超时 s.connect(new InetSocketAddress("169.254.77.36", 8888),10000); |
5.加入多线程
前面服务器端和客户端只是进行了简单的通信操作:服务器接收到客户端连接之后,服 务器向客户端输出一个字符串,而客户端也只是读取服务器的字符串后就退出了。实际应用 中的客户端则可能需要和服务器端保持长时间通信,即服务器需要不断地读取客户端数据, 并向客户端写入数据;客户端也需要不断地读取服务器数据,并向服务器写入数据。
当使用传统BufferedReader的readLine()方法读取数据时,当该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,服务器应该为每个Socket单独启动一条线程,每条线程负责与一个客户端进行通信。
客户端读取服务器数据的线程同样会被阻塞,所以系统应该单独启动一条线程,该线程 专门负责读取服务器数据。
下面考虑实现一个简单的C/S聊天室应用,服务器端则应该包含多条线程,每个Socket 对应一条线程,该线程负责读取Socket对应输入流的数据(从客户端发送过来的数据),并 将读到的数据向每个Socket输出流发送一遍(将一个客户端发送的数据“广播”给其他客户 端),因此需要在服务器端使用List来保存所有的Socket。
下面是服务器端的实现代码,程序为服务器提供了两个类,一个是创建ServerSocket监 听的主类,另一个是负责处理每个Socket通信的线程类。
代码清单:
服务器端:
ServerSocket监听的主类:
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; /** * Description: * 创建ServerSocket监听的主类 * @author jph */ public class MyServer { // 定义保存所有Socket的ArrayList public static ArrayList<Socket> socketList = new ArrayList<Socket>(); public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(30000); while(true) { // 此行代码会阻塞,将一直等待别人的连接 Socket s = ss.accept(); socketList.add(s); // 每当客户端连接后启动一条ServerThread线程为该客户端服务 new Thread(new ServerThread(s)).start(); } } } |
负责处理每一个Socket通信的线程类:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; /** * Description: * 负责处理每一个Socket通信的线程类 * @author jph */ // 负责处理每个线程通信的线程类 public class ServerThread implements Runnable { // 定义当前线程所处理的Socket Socket s = null; // 该线程所处理的Socket所对应的输入流 BufferedReader br = null; public ServerThread(Socket s) throws IOException { this.s = s; // 初始化该Socket对应的输入流 br = new BufferedReader(new InputStreamReader( s.getInputStream() , "utf-8")); //② } public void run() { try { String content = null; // 采用循环不断从Socket中读取客户端发送过来的数据 while ((content = readFromClient()) != null) { // 遍历socketList中的每个Socket, // 将读到的内容向每个Socket发送一次 for (Socket s : MyServer.socketList) { OutputStream os = s.getOutputStream(); os.write((content + "\n").getBytes("utf-8")); } } } catch (IOException e) { e.printStackTrace(); } } // 定义读取客户端数据的方法 private String readFromClient() { try { return br.readLine(); } // 如果捕捉到异常,表明该Socket对应的客户端已经关闭 catch (IOException e) { // 删除该Socket。 MyServer.socketList.remove(s); //① } return null; } }
|
上面的服务器端线程类不断读取客户端数据,程序使用readFromCHent()方法来读取客户端数据,如果读取数据过程中捕获到IOException异常,则表明该Socket对应的客户端Socket 出现了问题(到底什么问题我们不管,反正不正常),程序就将该Socket从socketList中删除, 如readFromClient()方法中①号代码所示。
当服务器线程读到客户端数据之后,程序遍历socketList集合,并将该数据向socketList 集合中的每个Socket发送一次一该服务器线程将把从Socket中读到的数据向socketList中 的每个Socket转发一次,如run()线程执行体中的粗体字代码所示。
注:
上面的程序中②号粗体字代码将网络的字节榆入流转换为字符输入流时,指定了转换所用的字符串:UTF-8,这也是由于客户端写过来的数据是采用UTF-8 字符集进行编码的,所以此处的服务器端也要使用UTF-8字符集进行解码。当需 要编写跨平台的网络通信程序时,使用UTF-8字符集进行编码、解码是一种较好的解决方案。
每个客户端应该包含两条线程:一条负责生成主界面,并响应用户动作,并将用户输入 的数据写入Socket对应的输出流中:另一条负责读取Socket对应输入流中的数据(从服务器 发送过来的数据),并负责将这些数据在程序界面上显示出来。
客户端:
客户端程序同样是一个Android应用,因此需要创建一个Android项目,这个Android 应用的界面中包含两个文本框:一个用于接收用户输入,另一个用于显示聊天信息:界面中 还有一个按钮,当用户单击该按钮时,程序向服务器发送聊天信息。该程序的界面布局代码 如下。
/** * 客户端: * */ public class MultiThreadClient extends Activity { // 定义界面上的两个文本框 EditText input; TextView show; // 定义界面上的一个按钮 Button send; Handler handler; // 定义与服务器通信的子线程 ClientThread clientThread; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); input = (EditText) findViewById(R.id.input); send = (Button) findViewById(R.id.send); show = (TextView) findViewById(R.id.show); handler = new Handler() //① { @Override public void handleMessage(Message msg) { // 如果消息来自于子线程 if (msg.what == 0x123) { // 将读取的内容追加显示在文本框中 show.append("\n" + msg.obj.toString()); } } }; clientThread = new ClientThread(handler); // 客户端启动ClientThread线程创建网络连接、读取来自服务器的数据 new Thread(clientThread).start(); //① send.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { try { // 当用户按下发送按钮后,将用户输入的数据封装成Message, // 然后发送给子线程的Handler Message msg = new Message(); msg.what = 0x345; msg.obj = input.getText().toString(); clientThread.revHandler.sendMessage(msg); // 清空input文本框 input.setText(""); } catch (Exception e) { e.printStackTrace(); } } }); } } |
代码分析:
当用户单击该程序界而中的“发送”按钮之后,程序将会把input输入框中的的内容发 送该clientThread的revHandler对象,clientThread将负责将用户输入的内容发送给服务器。
为了避免UI线程被阻塞,该程序将建立网络连接、与网络服务器通信等工作都交给 ClientThread线程完成。因此该程序在①号代码处启动ClientThread线程。
由于Android不允许子线程访问界面组件,因此上面的程序定义了一个Handler来处理 来自子线程的消息,如程序中②号粗体字代码所示。
ClientThread子线程负责建立与远程服务器的连接,并负责与远程服务器通信,读到数 据之后便通过Handler对象发送一条消息:当ClientThread子线程收到UI线程发送过来的消 息(消息携带了用户输入的内容)之后,还负责将用户输入的内容发送给远程服务器。该子 线程代码如下:
public class ClientThread implements Runnable { private Socket s; // 定义向UI线程发送消息的Handler对象 private Handler handler; // 定义接收UI线程的消息的Handler对象 public Handler revHandler; // 该线程所处理的Socket所对应的输入流 BufferedReader br = null; OutputStream os = null;
public ClientThread(Handler handler) { this.handler = handler; }
public void run() { try { //192.168.191.2为本机的ip地址,30000为与MultiThreadServer服务器通信的端口 s = new Socket("192.168.191.2", 30000); br = new BufferedReader(new InputStreamReader( s.getInputStream())); os = s.getOutputStream(); // 启动一条子线程来读取服务器响应的数据 new Thread() { @Override public void run() { String content = null; // 不断读取Socket输入流中的内容。 try { while ((content = br.readLine()) != null) { // 每当读到来自服务器的数据之后,发送消息通知程序界面显示该数据 Message msg = new Message(); msg.what = 0x123; msg.obj = content; handler.sendMessage(msg); } } catch (IOException e) { e.printStackTrace(); } } }.start(); // 为当前线程初始化Looper Looper.prepare(); // 创建revHandler对象 revHandler = new Handler() { @Override public void handleMessage(Message msg) { // 接收到UI线程中用户输入的数据 if (msg.what == 0x345) { // 将用户在文本框内输入的内容写入网络 try { os.write((msg.obj.toString() + "\r\n") .getBytes("utf-8")); } catch (Exception e) { e.printStackTrace(); } } } }; // 启动Looper Looper.loop(); } catch (SocketTimeoutException e1) { System.out.println("网络连接超时!!"); } catch (Exception e) { e.printStackTrace(); } } } |
实例分析:
上面线程的功能也非常简单,它只是不断获取Socket输入流中的内容,当读到Socket 输入流中的内容后,便通过Handler对象发送一条消息,消息负责携带读到数据,除此之外,该子线程还负责读取UI线程发送的消到消息之后,该子线程负责将消息中携带的数据发送给远程服务器。
先运行上面程序中的MyServer类,该类运行后只是作为服务器,看不到任何输出。接 着可以运行Android客户端一相当于启动聊天室客户端登录该服务器,接着可以看到在任 何一个Android客户端输入一些内容后单击“发送”按钮,将可看到所有客户端(包括自己) 都会收到他刚刚输入的内容,如上图所示,这就粗略实现了一个C/S结构聊天室的功能。