Android简单的聊天室开发(client与server沟通)

时间:2022-09-01 05:49:15

请尊重他人的劳动成果。转载请注明出处:Android开发之简单的聊天室(client与server进行通信)



1. 预备知识:Tcp/IP协议与Socket


TCP/IP
是Transmission Control Protocol/Intemet Protocol的简写,中文译名为传输控制协议/因特网互联协议。又叫网络通信协议,这个协议是Internet最主要的协议,是Internet国际互联网络的基础,简单地说,就是由网络层的IP协议和传输层的TCP协议组成的。

TCP/IP协议遵循的是一个抽象的分层模型,这个模型中全部的TCP/IP系列网络协议都被归类到4个抽象的“层”中。

每一抽象层建立在低一层提供的服务上,而且为高一层提供服务。完毕一些特定的任务须要众多的协议协同工作。这些协议分布在參考模型的不同层中,因此有时称它们为一个协议栈。

TCP/IP參考模型从下到上分别包含网络接口层、网络互连层、传输层、应用层四层常常使用的包含HTTP (万维网服务)、FTP
(文件传输)、SMTP (电子邮件)、SSH
(安全远程登录)、DNS (IP地址寻找)在内的很多协议都被觉得执行在TCP/IP协议栈的应用层之上。每一个应用层协议一般都会使用到两个传输层协议之中的一个:面向连接的TCP传输控制协议和无连接的包传输UDP用户数据报文协议。

传输层的协议可以解决诸如端到端可靠性(数据是否已经到达目的地)和保证数据依照正确的顺序到达这种问题。

TCP是一个“可靠的”、面向连接的传输机制,它提供一种可靠的字节流保证数据完整、无损而且按顺序到达。TCP尽置连续不断地測试网络的负载而且控制发送数据的速度以避免网络过载。另外。TCP试图将数据依照规定的顺序发送。

这是它与UDP不同之处,但这在实时数据流或者路由高网络层丢失率应用的时候可能成为一个缺陷。

UDP是一个无连结的数据报协议。它是一个“尽最大努力交付”或者“不可靠”协议。不是由于它特别“不可靠”。而是由于它不检查数据包是否已经到达目的地,而且不保证它们按顺序到达。假设一个应用程序须要这些特点,它必须自己提供或者使用TCP。

UDP的典型应用是如流媒体(音频和视频等)这样按时到达比可靠性更重要的应用,或者如DNS査找这样的简单查询/响应应用,假设建立可靠的连接则所做的额外工作将是非常多的。

在网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通经常使用来实现客户方和服务方的连接。

Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个port号唯一确定。

在传统的UNIX环境下能够操作TCP/IP协议的接口不止Socket—个,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必定联系的。

只是在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。

也就是说在Java环境下我们实现基于TCP/IP协议的网络编程须要採用
Socket机制。

Socket编程比基于URL的网络编程提供了更高的传输效率、更强大的功能和更灵活的控制,可是却要更复杂一些。因为Java本身的特殊性,Socket编程在Java中可能已经是层次最低的网络编程接口,在Java屮要直接操作协议中更低的层次,须要使用Java的本地方法调用
(JNI),在这里就不予讨论了。

Android中进行Socket编程与普通Java程序所进行的Socket编程的方式保持一致。不同的是数据的来源以及显示上有所差别。採用Java语言开发的一些网络编程的应用比方最经典的聊天室应用能够非常easy地移植到Android平台上。而因为TCP协议要比UDP协议的应用广泛。如经常使用的HTTP、FTP、SMTP等协议都是採用TCP协议,因此这里主要介绍Android中基于
TCP协议的Socket编程。

Socket通经常使用来实现C/S结构。使用Socket进行Client/Server程序设计的一般连接过程是这种:Server端监听某个port是否有连接请求。Client端向Server端发出连接请求,Server端向Client端发回Accept
(接受)消息。一个连接就建立起来了。

Server端和Client端都能够通过Send、Write等方法与对方通信。

Java在包java.net中提供了两个类Socket和ServerSocket。分别用来表示双向连接的client和服务端。

2.使用ServerSocket创建TCPserver端


Java中能接收其它通信实体连接请求的类是ServerSocket,ServerSocket对象用于监听来自client的Socket连接,假设没有连接,它将一直处于等待状态。ServerSocket包括一个监听来自client连接请求的方法。

1) Socket accept():假设接收到一个clientSocket的连接请求,该方法将返回一个与连接clientSocket相应的Socket;否则该方法将一直处于等待状态,线程也被堵塞。

创建ServerSocket对象。ServerSocket类提供了例如以下几个构造器:

2) ServerSocket(int port):用指定的port port来创建一个ServerSocket。

该port应该是有一个有效的port整数值:0〜65
535。

3) ServerSocket(int port,int backlog):添加一个用来改变连接队列长度的參数backlog。

4) ServerSocket(int port.int backlog,lnetAddresslocalAdd():在机器存在多个
IP地 址的情况下,同意通过localAddr这个參数来指定将ServerSocket绑定到指定的IP地址。

注:当ServerSocket使用完成后,应使用ServerSocket的close()方法来关闭该ServerSocket。

通常情况下,server不应该仅仅接收一个client请求,而应该不断地接收来自client的全部请求。

如以下代码所看到的:

//创建一个ServerSocket。用于监听client的连接请求

ServerSocket ss=new ServerSocket(1566);

//不停地从接收来自client的请求

while (true)
{

//每当接受一个来自client的Socket的请求,server端也相应产生一个Socket

Socket s=ss.accept();

//以下就能够使用Socket进行通信了

//..........

}

3.使用Socket进行通信


client通常可使用Socket的构造器来连接到指定server,Socket通常可使用例如以下两个构造器。

1) Socket(lnetAddress/String remoteAddress, int port):创建连接到指定远程主机、远程port的Socket。该构造器没有指定本地地址、本地port,默认使用本地主机的默认IP地址。默认使用系统动态指定的IP地址。

2) Socket(lnetAddress/String remoteAddress, int port, InetAddress localAddr,int localPort):创建连接到指定远程主机、远程port的Socket。并指定本地IP地址和本地port号,适用于本地主机有多个IP地址的情形。

上面两个构造器中指定远程主机时既可使用InetAddress来指定。也可直接使用String对象来指定。但程序通常使用String对象(如211.158.6.26)来指定远程IP。

当本地主机仅仅有—个IP地址时,使用第一个方法更为简单。

如:

Socket socket=new Socket("169.254.77.36",
8888);

//以下就能够和server进行通信了

当程序运行上面代码中的粗体字代码时,该代码将会连接到指定server。让server端的ServerSocket的accept()方法向下运行,于是server端和client就产生一对互相连接的Socket。

当client、server端产生了相应的Socket之后,程序无须再区分server、client,而是通过各自的Socket进行通信。Socket提供例如以下两个方法来获取输入流和输出流:

1) InputStreamgetlnputStream():返回该Socket对象相应的输入流,让程序通过该输入流从Socket中取出数据。

2) OutputStreamgetOutputStream():返回该Socket对象相应的输出流。让程序通过该输出流向Socket中输出数据。

4.实例:和server进行简单通信:


server端:

publicstaticvoid
main(String[] args) {

// TODO Auto-generated method stub

try {

//创建一个ServerSocket。用于监听client的连接请求

ServerSocket ss=new ServerSocket(8888);

//不停地从接收来自client的请求

while (true)
{

//每当接受一个来自client的Socket的请求,server端也相应产生一个Socket

Socket s=ss.accept();

//以下就能够使用Socket进行通信了

OutputStream os=s.getOutputStream();

os.write("来自server端的消息:你好。今天天气不错,骚年外出散散心吧。".getBytes("utf-8"));

//关闭输出流

os.close();

//关闭Socket

s.close();

}

} catch (Exception e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

注:上面的程序并未把OutputStream流包装成PrintStream。然后使用PrintStream直接输出整个字符串,这是由于该server端程序执行于Windows主机上,当直接使用PrintStream输出字符串时默认使用系统平台的字符串(即GBK
)进行编码;但该程序的client是Android应用,执行于Linux平台(Android是Linux内核的),因此当client读取网络数据时默认使用UTF-8字符集进行解码,这样势必引起乱码。

为了保证client能正常解析到数据,此处手动控制字符串的编码,强行指定使用UTF-8字符集进行编码,这样就能够避免乱码问

client:

edtMsg=(EditText)findViewById(R.id.edtMsg);

//创建并启动一个新线程。向server发送TCP请求

new Thread(){

@Override

publicvoid
run() {

// TODO Auto-generated method stub

super.run();

//创建一个Socket用于向IP为169.254.77.36的server的8888port发送请求

Socket s;

try {

s =new Socket();

//假设超过10s还没连接到server则视为超时

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

//设置client与server建立连接的超时时长为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-permissionandroid:name="android.permission.INTERNET"/>

程序执行效果图:

Android简单的聊天室开发(client与server沟通)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZmVuZ3l1emhlbmdmYW4=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="Android开发之简单的聊天室(client与server进行通信)1">

5.异常和捕捉


上面的程序为了突出通过ServerSocket和Socket建立连接并通过底层IO流进行通信的主题,程序没有进行异常处理,也没有使用finally块来关闭资源。

实际应用中。程序可能不想让运行网络连接、读取server数据的进程一直堵塞。而是希望当网络连接、读取操作超过合理时间之后,系统自己主动觉得该操作失败,这个合理时间就是超时时长。

Socket对象提供了一个setSoTimeout(int
timeout)来设置超时时长,如以下的代码片段所看到的:

//设置client与server建立连接的超时时长为30秒

s.setSoTimeout(30000);

为Socket对象指定了超时时长之后,假设使用Socket进行读、写操作完毕之前已经超出了该时间限制,那么这些方法就会抛出SocketTimeoutException异常,程序能够对该异常进行捕捉,并进行适当处理,例如以下面代码所看到的:

Socket s;

try {

s =new Socket();

//假设超过10s还没连接到server则视为超时

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

//设置client与server建立连接的超时时长为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连接server时指定超时时长:即经过指定时间后,如果该Socket还未连接到远程server,则系统觉得该Socket连接超时。但Socket的全部构造器里都没有提供指定超时时长的參数。所以程序应该先创建一个无连接Socket,再调用Socket的connect()方法来连接远程server。connect()方法就能够接受一个超时时长參数。

例如以下面代码所看到的:

//创建一个无连接的Socket

Socket s= new Socket();

//假设超过10s还没连接到server则视为超时

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

6.增加多线程


前面server端和client仅仅是进行了简单的通信操作:server接收到client连接之后。服务器向client输出一个字符串,而client也仅仅是读取server的字符串后就退出了。

实际应用中的client则可能须要和server端保持长时间通信,即server须要不断地读取client数据,并向client写入数据;client也须要不断地读取server数据,并向server写入数据。

当使用传统BufferedReader的readLine()方法读取数据时,当该方法成功返回之前,线程被堵塞。程序无法继续运行。

考虑到这个原因。server应该为每一个Socket单独启动一条线程,每条线程负责与一个client进行通信。

client读取server数据的线程相同会被堵塞,所以系统应该单独启动一条线程,该线程专门负责读取server数据。

以下考虑实现一个简单的C/S聊天室应用,server端则应该包括多条线程,每一个Socket相应一条线程。该线程负责读取Socket相应输入流的数据(从client发送过来的数据),并将读到的数据向每一个Socket输出流发送一遍(将一个client发送的数据“广播”给其它客户端),因此须要在server端使用List来保存全部的Socket。

以下是server端的实现代码。程序为server提供了两个类。一个是创建ServerSocket监听的主类,还有一个是负责处理每一个Socket通信的线程类。

代码清单:

server端:

ServerSocket监听的主类:

import java.io.IOException;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.ArrayList;

/**

* Description:

* 创建ServerSocket监听的主类

*@author jph

*/

publicclass
MyServer

{

// 定义保存全部Socket的ArrayList

publicstatic
ArrayList<Socket>socketList

= new ArrayList<Socket>();

publicstaticvoid
main(String[] args)

throws IOException

{

ServerSocket ss = new ServerSocket(30000);

while(true)

{

// 此行代码会堵塞。将一直等待别人的连接

Socket s = ss.accept();

socketList.add(s);

// 每当client连接后启动一条ServerThread线程为该client服务

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

*/

// 负责处理每一个线程通信的线程类

publicclass
ServerThreadimplements 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"));  //②

}

publicvoid
run()

{

try

{

String content =null;

// 採用循环不断从Socket中读取client发送过来的数据

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();

}

}

// 定义读取client数据的方法

private String readFromClient()

{

try

{

returnbr.readLine();

}

// 假设捕捉到异常。表明该Socket相应的client已经关闭

catch (IOException e)

{

// 删除该Socket。

MyServer.socketList.remove(s);   //①

}

returnnull;

}

}

上面的server端线程类不断读取client数据,程序使用readFromCHent()方法来读取client数据。假设读取数据过程中捕获到IOException异常。则表明该Socket相应的clientSocket出现了问题(究竟什么问题我们无论,反正不正常)。程序就将该Socket从socketList中删除,如readFromClient()方法中①号代码所看到的。

当server线程读到client数据之后。程序遍历socketList集合,并将该数据向socketList集合中的每一个Socket发送一次一该server线程将把从Socket中读到的数据向socketList中的每一个Socket转发一次,如run()线程运行体中的粗体字代码所看到的。

注:

上面的程序中②号粗体字代码将网络的字节榆入流转换为字符输入流时,指定了转换所用的字符串:UTF-8,这也是因为client写过来的数据是採用UTF-8字符集进行编码的。所以此处的server端也要使用UTF-8字符集进行解码。当需        要编写跨平台的网络通信程序时,使用UTF-8字符集进行编码、解码是一种较好的解决方式。

每一个client应该包括两条线程:一条负责生成主界面,并响应用户动作。并将用户输入的数据写入Socket相应的输出流中:还有一条负责读取Socket相应输入流中的数据(从server发送过来的数据)。并负责将这些数据在程序界面上显示出来。

client:

client程序相同是一个Android应用。因此须要创建一个Android项目,这个Android应用的界面中包括两个文本框:一个用于接收用户输入,另一个用于显示聊天信息:界面中另一个button。当用户单击该button时。程序向server发送聊天信息。该程序的界面布局代码例如以下。

/**

* client:

* */

publicclass
MultiThreadClientextends Activity

{

// 定义界面上的两个文本框

EditText input;

TextView show;

// 定义界面上的一个button

Button send;

Handler handler;

// 定义与server通信的子线程

ClientThread clientThread;

@Override

publicvoid
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

publicvoid
handleMessage(Message msg)

{

// 假设消息来自于子线程

if (msg.what
== 0x123)

{

// 将读取的内容追加显示在文本框中

show.append("\n"
+ msg.obj.toString());

}

}

};

clientThread =new
ClientThread(handler);

// client启动ClientThread线程创建网络连接、读取来自server的数据

new Thread(clientThread).start();//①

send.setOnClickListener(new
OnClickListener()

{

@Override

publicvoid
onClick(View v)

{

try

{

// 当用户按下发送button后,将用户输入的数据封装成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();

}

}

});

}

}

代码分析:

当用户单击该程序界而中的“发送”button之后,程序将会把input输入框中的的内容发送该clientThread的revHandler对象,clientThread将负责将用户输入的内容发送给server。

为了避免UI线程被堵塞,该程序将建立网络连接、与网络server通信等工作都交给 ClientThread线程完毕。

因此该程序在①号代码处启动ClientThread线程。

因为Android不同意子线程訪问界面组件。因此上面的程序定义了一个Handler来处理来自子线程的消息,如程序中②号粗体字代码所看到的。

ClientThread子线程负责建立与远程server的连接,并负责与远程server通信,读到数据之后便通过Handler对象发送一条消息:当ClientThread子线程收到UI线程发送过来的消息(消息携带了用户输入的内容)之后,还负责将用户输入的内容发送给远程server。

该子线程代码例如以下:

publicclass
ClientThreadimplements Runnable

{

private Sockets;

// 定义向UI线程发送消息的Handler对象

private Handlerhandler;

// 定义接收UI线程的消息的Handler对象

public HandlerrevHandler;

// 该线程所处理的Socket所相应的输入流

BufferedReader br =null;

OutputStream os =null;

public ClientThread(Handler handler)

{

this.handler
= handler;

}

publicvoid
run()

{

try

{

//192.168.191.2为本机的ip地址,30000为与MultiThreadServerserver通信的port

s =new
Socket("192.168.191.2", 30000);

br =new
BufferedReader(new InputStreamReader(

s.getInputStream()));

os =s.getOutputStream();

// 启动一条子线程来读取server响应的数据

new Thread()

{

@Override

publicvoid
run()

{

String content =null;

// 不断读取Socket输入流中的内容。

try

{

while ((content
=br.readLine()) !=null)

{

/span>// 每当读到来自server的数据之后,发送消息通知程序界面显示该数据

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

publicvoid
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简单的聊天室开发(client与server沟通)

实例分析:

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

先执行上面程序中的MyServer类。该类执行后仅仅是作为server,看不到不论什么输出。接着能够执行Androidclient一相当于启动聊天室client登录该server。接着能够看到在任何一个Androidclient输入一些内容后单击“发送”button。将可看到全部client(包含自己)都会收到他刚刚输入的内容。如上图所看到的。这就粗略实现了一个C/S结构聊天室的功能。

版权声明:本文博主原创文章,博客,未经同意不得转载。