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 //不停地从接收来自客户端的请求 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) { // 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) { // 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 // super.run(); //创建一个Socket用于向IP为169.254.77.36的服务器的8888端口发送请求 Socket s; try { s = new Socket(); //如果超过10s还没连接到服务器则视为超时 s.connect(new InetSocketAddress("169.254.77.36", //设置客户端与服务器建立连接的超时时长为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) { // e.printStackTrace(); }catch (Exception e) { // 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", //设置客户端与服务器建立连接的超时时长为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= //如果超过10s还没连接到服务器则视为超时 s.connect(new |
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: * * */ public { // public = new ArrayList<Socket>(); public throws IOException { ServerSocket ss = new ServerSocket(30000); while(true) { // Socket s = ss.accept(); socketList.add(s); // new Thread(new } } } |
负责处理每一个Socket通信的线程类:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; /** * Description: * * */ // public { // Socket s = // BufferedReader br = public ServerThread(Socket s) throws IOException { this.s // br = s.getInputStream() , "utf-8")); } public { try { String content = null; // while ((content = readFromClient()) != { // // 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 } // catch (IOException e) { // MyServer.socketList.remove(s); } return } } |
上面的服务器端线程类不断读取客户端数据,程序使用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 { // EditText input; TextView show; // Button send; Handler handler; // ClientThread clientThread; @Override public { 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 = { @Override public { // if (msg.what { // show.append("\n" } } }; clientThread = // new Thread(clientThread).start(); send.setOnClickListener(new { @Override public { try { // // Message msg = new Message(); msg.what = 0x345; msg.obj = clientThread.revHandler.sendMessage(msg); // 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 // private Handler // public Handler // BufferedReader br = OutputStream os = public ClientThread(Handler handler) { this.handler } public { try { //192.168.191.2为本机的ip地址,30000为与MultiThreadServer服务器通信的端口 s = br = s.getInputStream())); os = // new Thread() { @Override public { String content = null; // try { while ((content = { // Message msg = new Message(); msg.what = 0x123; msg.obj = content; handler.sendMessage(msg); } } catch (IOException e) { e.printStackTrace(); } } }.start(); // Looper.prepare(); // revHandler = { @Override public { // if (msg.what { // try { os.write((msg.obj.toString() .getBytes("utf-8")); } catch (Exception e) { e.printStackTrace(); } } } }; // Looper.loop(); } catch (SocketTimeoutException e1) { System.out.println("网络连接超时!!"); } catch (Exception e) { e.printStackTrace(); } } } |
实例分析:
上面线程的功能也非常简单,它只是不断获取Socket输入流中的内容,当读到Socket
输入流中的内容后,便通过Handler对象发送一条消息,消息负责携带读到数据,除此之外,该子线程还负责读取UI线程发送的消到消息之后,该子线程负责将消息中携带的数据发送给远程服务器。
先运行上面程序中的MyServer类,该类运行后只是作为服务器,看不到任何输出。接
着可以运行Android客户端一相当于启动聊天室客户端登录该服务器,接着可以看到在任
何一个Android客户端输入一些内容后单击“发送”按钮,将可看到所有客户端(包括自己)
都会收到他刚刚输入的内容,如上图所示,这就粗略实现了一个C/S结构聊天室的功能。