Android开发之简单的聊天室(客户端与服务器进行通信)

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使用完毕后,应使用ServerSocketclose()方法来关闭该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);

//下面就可以和服务器进行通信了

当程序执行上面代码中的粗体字代码时,该代码将会连接到指定服务器,让服务器端的ServerSocketaccept()方法向下执行,于是服务器端和客户端就产生一对互相连接的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用于向IP169.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"/>

程序运行效果图:

Android开发之简单的聊天室(客户端与服务器进行通信)


4.异常和捕捉


上面的程序为了突出通过ServerSocketSocket建立连接并通过底层 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,再调用Socketconnect() 方法来连接远程服务器,connect()方法就可以接受一个超时时长参数。如以下代码所示:

//创建一个无连接的Socket

Socket s= new Socket();

//如果超过10s还没连接到服务器则视为超时

s.connect(new InetSocketAddress("169.254.77.36", 8888),10000);


5.加入多线程



         前面服务器端和客户端只是进行了简单的通信操作:服务器接收到客户端连接之后,服 务器向客户端输出一个字符串,而客户端也只是读取服务器的字符串后就退出了。实际应用 中的客户端则可能需要和服务器端保持长时间通信,即服务器需要不断地读取客户端数据, 并向客户端写入数据;客户端也需要不断地读取服务器数据,并向服务器写入数据。

当使用传统BufferedReaderreadLine()方法读取数据时,当该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,服务器应该为每个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

{

    // 定义保存所有SocketArrayList

    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 出现了问题(到底什么问题我们不管,反正不正常),程序就将该SocketsocketList中删除, 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输入框中的的内容发 送该clientThreadrevHandler对象,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();

        }

    }

}

 

Android开发之简单的聊天室(客户端与服务器进行通信)

实例分析:

上面线程的功能也非常简单,它只是不断获取Socket输入流中的内容,当读到Socket 输入流中的内容后,便通过Handler对象发送一条消息,消息负责携带读到数据,除此之外,该子线程还负责读取UI线程发送的消到消息之后,该子线程负责将消息中携带的数据发送给远程服务器。

先运行上面程序中的MyServer类,该类运行后只是作为服务器,看不到任何输出。接 着可以运行Android客户端一相当于启动聊天室客户端登录该服务器,接着可以看到在任 何一个Android客户端输入一些内容后单击“发送”按钮,将可看到所有客户端(包括自己) 都会收到他刚刚输入的内容,如上图所示,这就粗略实现了一个C/S结构聊天室的功能。

Android开发之简单的聊天室(客户端与服务器进行通信)

上一篇:记录一次学习制作rpm包的过程


下一篇:从零开始学android