第18回 一般人不能理解Socket的奥妙

自从帮吕布搞定音视频播放之后,刘关张三人发现帮人代做项目也是蛮有搞头的,大钱虽然赚不到,但是至少能够解决温饱问题。但郁闷的是,自吕布走后再也没有这种傻子送上门。于是,关羽建议,不如去网上找找项目。

于是三人找呀找,找呀找……屁都没找到。

这时孔明来了,“看你们三个跟傻子似的,果然少了我这个拉皮条的就是不行啊!”

刘备一把抱住孔明的大腿:“明明,不要抛弃我们!”

孔明:“正好我有个朋友在一家网络公司上班,现在需要有人帮忙为他们做个Android的移动应用,方便网站用户使用。不过你们要做的话得出趟差,去他们公司总部一趟。”

刘备:“出差不怕,我们也在你这里宅了许久了,也是时候出门走走了,话说你朋友叫啥,公司在哪?”

孔明:“他名叫鲁肃,公司在江东。”

张飞:“鲁肃?跟鲁迅到底什么关系?”

孔明:“有你妹关系!”

张飞:“原来是这样!江东远不远啊,来回火车票能报销不?”

孔明:“远倒是有点远,不过人家公司有的是钱,别说火车票,高铁票都给报销。”

刘关张齐道:“哇~我们去!”

孔明:“对方是网络公司,做的东西跟网络怕是脱不了干系,给你们一个锦囊,里面包含了Android网络编程之要义。你们之前都是做单机应用比较多,一定要利用路上的时间好好掌握网络开发的技巧!”

张飞:“二哥,听说江东妹子很水啊~”

关羽:“你没看见大哥一个劲儿的流口水嘛。”

孔明:“你们到底有没有听我说话……”

1.1. 网络通信的基础知识

网络,是用物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的。网络通信是人与人之间同过网络这个媒体进行信息交流与传递的过程。

网络由物理层、数据链路层、网络层、传输层、会话层、表示层和应用层七层组成,Socket主要实现系统协议栈部分,包含了网络层、传输层等。例如,如果直接调用RawSocket类重新编写一个类似TCP的协议,此时Socket编程应当属于传输层上的编程;如果直接使用TCP/UDP等协议编写网络应用,那应该就属于应用层上的编程。

Socket常用于TCP/UDP协议通信。TCP协议和UDP协议都属于传输层协议。TCP/UDP协议推动了客户/服务器通信模式的广泛运用。在通信的两个进程中,一个进程为客户进程,另一个进程为服务器进程。客户进程向服务器进程发出要求某种服务的请求,服务器进程响应用请求。如图18-1所示,通常,一个服务器进程会同时为多个客户进程提供服务,图中服务器进程B1同时为客户进程A1、A2和B2提供服务。

图18-1客户进程A1、A2和B2请求服务器进程B1

1.2. Android中的Socket通信

Android平台有三种网络接口可以使用,它们分别是:java.net.*(标准Java接口)、Apache接口和Android.net.*(Android网络接口)。下面分别介绍这些接口的功能和作用:

l  标准Java接口:java.net.*提供与联网有关的类,包括流、数据包套接字(Socket)、Internet协议、常见HTTP处理等。

l  Apache接口:Android提供的Apache HttpClient是一个开源项目,功能更加完善,为客户端的HTTP编程提供高效、最新、功能丰富的工具包支持。

l  Android.net编程:常常使用Android.net包下的类进行Android特有的网络编程,例如访问WiFi、访问Android联网信息、邮件等功能。

Socket通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过“套接字”向网络发出请求或者应答网络请求。Socket在建立网络连接时使用。在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。

1.2.1.构造Socket

Socket的构造方法主要有以下两种重载形式:

l  Socket(InetAddressaddress, int port)

l  Socket(Stringhost, int port)

这两种构造方法都会试图建立与服务器的连接,如果连接成功,就会返回Socket对象;如果因为某些原因连接失败了,则会抛出IOException。

 

设定等待建立连接的超时时间

当客户端的Socket构造方法请求与服务器连接时,可能要等待一段时间。默认情况下,Socket的构造方法会一直等待下去,直到连接成功,但是受底层网络的传输速度的影响,Socket可能会处于长时间的等待状态。

Socket类的connect(SocketAddress endpoint,int timeout)方法负责连接服务器,endpoint指定服务器的地址,timeout设定超时时间(毫秒),如果设为0,表示永远不会超时。使用connect()方法限定等待连接的时间,其代码如下所示:

Socketsocket=new Socket();

SocketAddressremoteAddr=new InetSocketAddress("localhost",5000);

socket.connect(remoteAddr,600000);

上述代码用于将Socket连接到本地端口为5000的服务器程序,等待连接的最长时间为1分钟。如果在1分钟内出现某种异常,则抛出异常;如果超过1分钟后,既没有连接成功,也没有出现其他异常,那么会抛出SocketTimeoutException。

 

设定服务器的地址

在构造Socket时需要在参数中设定服务器的IP地址、主机名和端口。在前面介绍的两种构造方法中,InetAddress类表示服务器的IP地址。InetAddress类提供了一系列方法用于构造自身的实例,例如:

//返回本地主机的IP地址

InetAddressaddr1=InetAddress.getLocalHost();

//返回代表"192.168.1.50"的IP地址

InetAddressaddr2=InetAddress.getByName("192.168.1.50");

//返回域名为"www.firstpeople.com"的IP地址

InetAddressaddr3=InetAddress.getByName("www.firstpeople.com");

 

设定客户端的地址

一个Socket既包含了服务器的IP地址和端口号,也包含了客户端的IP地址和端口号。默认情况下,在构造Socket时,客户端的IP地址为主机的IP地址,而客户端的端口由操作系统随机分配。Socket类还可以显式地设置客户端的IP地址和端口,如下代码所示:

//参数localAddr和localPort用来设置客户端的IP地址和端口

Socket(InetAddressaddress, int port, InetAddress localAddr, int localPort)

Socket(Stringhost, int port, InetAddress localAddr, int localPort)

 

连接服务器时可能抛出的异常

当Socket的构造方法请求连接服务器时,可能会抛出下面的异常:

l  UnknownHostException:无法识别主机的名字或IP地址。

l  ConnectException:没有服务器进程监听指定的端口,或者服务器进程拒绝连接。

l  SocketTimeoutException:等待连接超时。

l  BindException:无法把Socket对象与指定的本地IP地址或端口绑定。

 

孔明:在构造Socket时客户端会根据IP地址和端口直接连接服务器,由于网络环境的不确定性,很可能会抛出异常,因此在构造Socket时需要进行异常处理

 

 

 


1.2.2.获取Socket的信息

在一个Socket中包含了远程服务器端的IP地址和端口号,以及客户端的IP地址和端口号。此外,从Socket中还可以获得输入流和输出流,分别用于向服务器发送数据,接收数据。以下方法用于获取Socket的有关信息:

l  getInetAddress():获得远程服务器的IP地址。

l  getPort():获得远程服务器的端口。

l  getLocalAddress():获得客户本地的IP地址。

l  getLocalPort():获得客户本地的端口。

l  getInputStream():获得输入流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownInput()方法关闭输入流,那么此方法会抛出IOException。

l  getOutputStream():获得输出流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput()方法关闭输出流,那么此方法也会抛出IOException。

在Andorid的网络编程中,这些Socket的信息可以用于控制数据包的发送与接收。

1.2.3.关闭Socket

当客户端与服务器的通信结束时,应该及时关闭Socket释放Socket占用的端口、内存等资源。Socket的close()方法负责关闭Socket。当一个Socket对象被关闭后,就不能再通过它的输入流和输出流进行I/O操作,否则会导致IOException。

为了确保关闭Socket的操作总是被执行,Socket的关闭代码应该位于final代码块中,例如:

Socket socket=null;

try{

//构造socket并连接服务器

}catch(IOException){

//异常处理

}final{

           if(socket!=null)

//关闭Socket

socket.close();

}

           在关闭Socket前,常常需要知道Socket当前的状态,Socket提供了3个状态检测方法:

l  isClosed():如果Socket已经连接到远程主机并还没有关闭,则返回true,否则返回false。

l  isConnected():如果Socket曾经连接到远程主机,则返回true,否则返回false。

l  isBound():如果Socket已经与一个本地端口绑定,则返回true,否则返回false。

1.2.4.半关闭Socket

当调用Socket的close()方法关闭Socket时,它的输入流和输出流也都被关闭。有些时候,我们希望关闭Socket中的输入流或输出流之一,此时就需要Socket中的半关闭方法:

l  shutdownInput():关闭输入流。

l  shutdownOutput():关闭输出流。

张飞:大哥,是否先后调用shutdownInput()和shutdownOutput()关闭输入流和输出流后就相当于关闭了Socket?

刘备:先后调用shutdownInput()和shutdownOutput()方法仅仅关闭了输入流和输出流,并不等价于关闭了Socket。只有调用Socket中的close()方法才能释放Socket占用的资源。

 

 

 

 

 

 


Socket类还提供了两个状态测试方法,用来判断输入流和输出流是否关闭:

l  public boolean isInputShutdown():如果输入流关闭,则返回true,否则返回false。

l  public boolean isOutputShutdown():如果输出流关闭,则返回true,否则返回false。

1.3. 设置Socket的选项

Socket提供了以下几个选项用以更灵活地配置Socket的工作方式、等待时间和缓冲区大小等:

 

选项TCP_NODELAY

TCP_NODELAY表示立即发送数据,默认值为false,表示采用Negale算法。如果调用setTcpNoDelay(true)方法,就会关闭Socket的缓冲区,确保数据及时发送。如果Socket的底层实现不支持TCP_NODELAY选项,那么Socket.getTcpNoDelay()和Socket.setTcpNoDelay()方法会抛出SocketException。

 

选项SO_RESUSEADDR

SO_RESUSEADDR表示是否允许重用Socket所绑定的本地地址。为了确保一个进程关闭Socket后,即使它还没释放端口,同一个主机上的其他进程还可以立刻重用该端口,这时可以通过Socket的setResuseAddress(true)方法将SO_RESUSEADDR属性值设置为true。值得注意的是socket.setResuseAddress(true)方法必须在Socket还没有绑定到一个本地端口之前调用,否则执行socket.setResuseAddress(true)方法无效。此外,两个共用同一个端口的进程必须都调用socket.setResuseAddress(true)方法,才能使得一个进程关闭Socket后,另一个进程的Socket能够立刻重用相同端口。

 

选项SO_TIMEOUT

Socket类的SO_TIMEOUT选项用于设定接收端接收数据的等待超时时间(毫秒),默认值为0,表示会无限等待,永远不会超时。可以使用public void setSoTimeout(int milliseconds)方法设置该选项。

 

选项SO_LINGER

SO_LINGER选项用来设置Socket关闭时的行为。默认情况下,执行Socket的close()方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间,直到发送完所有剩余的数据,才会真正关闭Socket,断开连接。

 

选项SO_RCVBUF

SO_RCVBUF表示Socket的用于接收数据的缓冲区的大小。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的通信)可以使用较大的缓冲区,这可以有效减少传输数据的次数,提高传输数据的效率。而对于交互频繁且单次传送数据量比较小的通信方式(例如Telnet和网络游戏),则应该采用小的缓冲区,确保小批量的数据能及时发送给对方。这种设定缓冲区大小的原则也同样适用于Socket的SO_SNDBUF选项。如果底层Socket不支持SO_RCVBUF选项,那么Socket.setReceiveBufferSize()方法会抛出SocketException。

 

选项SO_SNFBUF

SO_SNDBUF表示Socket的用于传送数据的缓冲区的大小。如果底层Socket不支持SO_SNDBUF选项,setSendBufferSize()方法会抛出SocketException。

 

选项SO_KEEPALIVE

表示是否要自动关闭长时间处于空闲状态的Socket。SO_KEEPALIVE选项的默认值为false,表示Tcp不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃。当SO_KEEPALIVE选项为true时,表示底层的Tcp会监视该连接是否有效。当连接处于空闲状态(连接的两端没有互相传送数据时)超过2个小时时,本地的Tcp会发送一个数据包给远程的Socket。如果远程的Socket没有发回响应,Tcp就会持续尝试11分钟,直到接收到响应为止。如果在12分钟内未收到响应,Tcp就会自动关闭本地Socket,断开连接。在不同的网络平台上,Tcp尝试与远程Socket对话的时限会有所差别。

 

选项OOBINLINE

表示是否支持发送一个字节的Tcp紧急数据。当OOBINLINE为true时,表示支持发送一个字节的Tcp紧急数据。OOBINLINE的默认值为false,在这种情况下,当接收方收到紧急数据时不作任何处理,直接将其丢弃。

1.4. 非阻塞通信

使用Socket编写的Android程序在运行过程中常常会阻塞。例如当一个线程执行Socket中的read()方法接收数据时,如果输入流中没有数据,该线程就会一直等待读入了足够长的数据才会从read()方法返回。在实际编程中,常常需要非阻塞的Socket通信,所谓非阻塞,就是指当线程执行方法时,如果操作还没有就绪,就立即返回,而不会一直等到操作就绪。例如,当线程从输入流中读数据时,如果输入流中还没有数据,就立即返回;或者如果输入流还没有足够的数据,那么就读取现有的数据,然后返回。

非阻塞的通信机制主要由java.nio包中的类实现,主要的类包括ServerSocketChannel、SocketChannel、Selector、SelectionKey和ByteBuffer等。本节重点介绍如何使用java.nio包中的类来创建Android客户端程序。

1.4.1.Socket阻塞的原因

在使用Socket进行远程通信时,Android客户端的程序在以下情况下可能会进入阻塞。

l  客户端请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回。

l  线程从Socket的输入流读入数据时,如果没有足够的数据就会进入阻塞状态,直到读到了足够的数据,或者达到输入流的末尾,或者出现异常,才从输入流的read()方法返回或异常中断。

张飞:大哥!输入流中要多少数据才能算足够呢?

刘备:这就要看线程执行的read()方法的类型了,例如,int read():只要输入流中有一个字节,就算足够;int read(byte[] buff):只要输入流中的字节数目与参数buff数组的长度相同,就算足够;String readLine():只要输入流中有一行字符串,就算足够。

 

 

 

 

 


l  线程向Socket输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才会从输出流的write()方法返回或异常中断。

l  调用Socket的setSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket的close()方法时,就会进入阻塞状态,直到底层Socket发送完所有剩余的数据,或者超过了setSoLinger()方法设置的延迟时间,才从输出流的write()方法返回或异常中断。

1.4.2.缓冲区Buffer

缓冲区是一个暂时接收和发送数据的存储区域。所有的缓冲区都有以下属性:

l  容量:表示该缓冲区可以保存多少数据。

l  极限:表示缓冲区的当前终点,不能对缓冲区中超过极限的区域进行读写操作。

l  位置:表示缓冲区中下一个读写单元的位置,每次读写缓冲区的数据时,都会改变该值,为下一次读写数据作准备。

缓冲区中三个属性的关系为:容量大于极限,极限大于位置,如图18-2所示:

图18-2缓冲区的三个属性:容量、极限和位置

NIO中的Buffer类提供了以下几个方法用来控制缓冲区中的这三个属性,如下表18-1所示:

表18-1 Buffer类方法

方法

说明

clear()

把缓冲区中的极限设为容量,再把位置设为0。

flip()

把极限设为位置,再把位置设为0。

rewind()

把位置设为0。

remaining()

返回缓冲区的容量,取值等于(极限-位置)。

compact()

删除缓冲区中从0到位置的内容,然后把从位置到极限的内容复制到0到(极限-位置)的区域内。

get(int index)

从参数index指定的位置读取一个数据。

put(int index)

向参数index指定的位置写入一个数据。

1.4.3.字符串编码Charset

NIO中的Charset类的每个实例代表特定的字符串编码类型。把字符序列转换为字符串的过程称为解码;把字符串转换为字符序列的过程称为编码。编码提供了数据传输的安全性和可靠性,在网络上传输的都是经过编码后的数据。

Charset类提供了数据传输前编码与解码的方法,在实际的编程工作中,应该根据不同的情况采用不同的编码方式。Charset类中的主要方法如下所示:

l  ByteBuffer encode(String str):对参数str指定的字符串进行编码,把得到的字符串序列存放在一个ByteBuffer对象中,并将其返回。

l  ByteBuffer encode(CharBuffer cb):对参数cb指定的字符缓冲区中的字符进行编码,把得到的字节序列存放在一个ByteBuffer对象中,并将其返回。

l  CharBuffer decode(ByteBuffer bb):把参数bb指定的ByteBuffer中的字节序列进行解码,把得到的字符序列存放在一个CharBuffer对象中,并将其返回。

孔明:在Socket的编程中,经常碰到接收端收到乱码的问题,很有可能是客户端和服务器端对数据采用的编码方式不统一造成的。

 

 

 


1.4.4.通道Channel

通道用来连接缓冲区与发送或者接收的数据,NIO为通道提供了Channel接口并声明了两个方法:

l  close():关闭通道。

l  isOpen():判断通道是否打开。

Channel接口提供了两个重要的子接口ReadableByteChannel和WritableByteChannel。ReadableByteChannel接口声明了read(ByteBuffer)方法用来将数据源的数据读入指定的ByteBuffer缓冲区中;WritableByteChannel接口声明了write(ByteBuffer)方法将指定的ByteBuffer缓冲区中的数据写入数据汇中。

1.4.5.SocketChannel

NIO中的SocketChannel类可以看作是Socket的替代类,但它比Socket具有更多的功能。SocketChannel的主要方法如下所示:

l  public static SocketChannel open(SocketAddress remote):SocketChannel的静态工厂方法open()负责创建SocketChannel对象。

孔明:值得注意的是,open()方法返回的SocketChannel对象处于阻塞模式,如果希望它改为非阻塞模式,必须执行以下代码:SocketChannel.configureBlocking(false);。

 

 

 

 


l  public Socket socket():返回与该SocketChannel关联的Socket对象。每个SocketChannel对象都与一个Socket对象关联。

l  public boolean connect(SocketAddress):使底层Socket建立远程连接。当SocketChannel处于非阻塞模式时,如果连接成功,该方法返回true,如果不能连接成功,则返回false,需要继续调用finishConnect()方法来完成连接。当SocketChannel处于阻塞模式,如果连接成功,该方法返回true,如果不能连接成功,将进入阻塞状态,直到连接成功,或者出现I/O异常。

l  public int read(ByteBuffer):从Chanel中读入若干字节,存放到参数指定的ByteBuffer中。

l  public int write(ByteBuffer):把参数指定的ByteBuffer中的字节写到Channel中。

1.4.6.Selector

Selector类用于监听SocketChannel对其注册的事件。当SocketChannel调用register()方法时,会新建一个SelectionKey并加入到Selector的监听事件中,这个SelectionKey对象是用来跟踪注册事件的句柄。

如果关闭了与SelectionKey对象关联的Channel对象,或者调用了SelectionKey对象的cancel()方法,在下一次执行Selector的select()方法时,该SelectionKey对象就会从Selector的监听事件中删除。在执行Selector的select()方法时,如果与SelectionKey相关的事件发生了,这个SelectionKey就被加入到selected-keys集合中。程序调用remove()方法或者调用Iterator的remove()方法,都可以删除一个SelectionKey对象。

1.5. Socket实例

本节将通过一个实例介绍如何使用Socket。建立一个Activity,命名为SimpleSocket,代码如下所示:

SimpleSocket.java代码清单18-5-0:

/**

* @author关羽:三弟,你在哪?

         张飞:网吧。

                     关羽:王八?

*/

public class SimpleSocket extends Activityimplements OnClickListener{

           publicTextView mSrverTxt = null;

           publicTextView mClientTxt = null;

           staticSocket mClient;

           ServerSocketmSever;

           SocketmSocket;

           @Override

           publicvoid onCreate(Bundle savedInstanceState) {

                     super.onCreate(savedInstanceState);

                     setContentView(R.layout.main);

                     ButtonbtnStart = (Button)findViewById(R.id.StartComm);

                     ButtonbtnStop = (Button)findViewById(R.id.StopComm);  

                     mSrverTxt= (TextView)findViewById(R.id.ServerInfo); 

                     mClientTxt= (TextView)findViewById(R.id.ClientInfo); 

                     //设置两个Button的监听事件

                     btnStart.setOnClickListener(this);

                     btnStop.setOnClickListener(this);

           }

           publicvoid onClick(View v) {

                     switch(v.getId()) {

                     caseR.id.StartComm:

                                Start();

                                break;

                     caseR.id.StopComm:

                                Stop();

                                break;

                     default:

                                break;

                     }

           }

           privateint Start(){

                     //启动Server线程

                     ServerStart();

                     //启动Client线程

                     ClientStart();

                     return0;

           }

           privateint Stop(){

                     //停止Socket线程

                     ServerStop();

                     //停止Client线程

                     ClientStop();

                     return0;

           }

           privatevoid ServerStart() {

                     //服务器监听端口号

                     intserverPort = 12345;

                     try{

                                //创建一个ServerSocket对象,并让这个Socket在12345端口监听

                                mSever= new ServerSocket(serverPort);       

                     }catch (IOException e) {

                                e.printStackTrace();

                     }

                     //开始监听

                     BeginListen();

           }

           privatevoid ServerStop() {

                     try{

                                mSever.close();

                     }catch(IOExceptione){

                                e.printStackTrace();

                     }

           }

           publicvoid BeginListen(){

                     //启动监听线程

                     newThread(new Runnable(){

                                //定义监听线程

                                publicvoid run(){

                                          BufferedReaderinBuf;

                                          try{

                                                     //获取连接的Socket

                                                     mSocket= mSever.accept();

                                          }catch(SocketExceptione){

                                                     e.printStackTrace();

                                          }catch(IOException e){

                                                     e.printStackTrace();

                                          }

                                          try{

                                                     //接收缓冲区

                                                     inBuf=new BufferedReader(

new InputStreamReader(mSocket.getInputStream(),"UTF-8"));

                                                     //发送缓冲区

                                                     PrintWriterout = new PrintWriter(mSocket.getOutputStream());

                                                     while(!mSocket.isClosed()){

                                                               Stringstr;

                                                               //接收从客户端发来的字符串

                                                               str= inBuf.readLine();

                                                               //封装数据

                                                                Message mess = Message.obtain();  

                                                               mess.obj= str;  

                                                               //发送给Handle处理

                                                               ServerHandler.sendMessage(mess); 

                                                               //服务器发送回给客户端的数据

                                                               out.println("服务器返回的消息");

                                                               out.flush();

                                                               //判断字符串是否为空

                                                               if(str == null || str.equals("end"))

                                                                          break;

                                                     }

                                                     //关闭Socket

                                                     mSocket.close();

                                          }catch(IOExceptione){

                                                     e.printStackTrace();

                                          }

                                }

                     }).start();

           }

           privatevoid ClientStart(){

                     //创建一个线程

                     newThread(new Runnable(){

                                publicvoid run(){

                                          try{

                                                     //创建一个Socket,并连接本地端口为12345的服务器

                                                     mClient= new Socket("127.0.0.1",12345);

                                          }catch(UnknownHostException e){

                                                     e.printStackTrace();

                                          }catch(IOException e){

                                                     e.printStackTrace();

                                          }

                                          //发送消息,并接收从服务器返回的消息

                                          Stringstr=SendMsg("客户端发送的消息");      

                                           // 封装数据

                                          Messagemessage = Message.obtain();  

                                          message.obj= str;  

                                          //利用ClientHandle处理服务器返回的消息

                                          ClientHandler.sendMessage(message);  

                                }

                     }).start();

           }

           privatevoid ClientStop() {

                     try{

                                //关闭Socket

                                mClient.close();

                     }catch(IOExceptione){

                                e.printStackTrace();

                     }

           }

           publicString SendMsg(String msg){

                     try{

                                BufferedReaderin = new BufferedReader(

newInputStreamReader(mClient.getInputStream()));

                                PrintWriterout = new PrintWriter(mClient.getOutputStream());

                                out.println(msg);

                                out.flush();

                                returnin.readLine();

                     }catch(IOExceptione){

                                e.printStackTrace();

                     }

                     return"";

           }

           privateHandler ServerHandler = new Handler() {

                     @Override

                     publicvoid handleMessage(Message msg) {

                                //获取封装在Message对象中的信息

                                Strings = (String) msg.obj;  

                                //在主线程中对组件进行操作

                                mSrverTxt.append("服务器接收到的信息为:" + s +'\n');  

                     }

           };

           privateHandler ClientHandler = new Handler() {

                     @Override

                     publicvoid handleMessage(Message msg) {

                                //获取封装在Message对象中的信息

                                Strings = (String) msg.obj;  

                                //在主线程中对组件进行操作

                                mClientTxt.append("服务器返回的信息为:" + s +'\n');  

                     }

           };

}

上述代码建立了一个客户端线程和一个服务器端线程。服务器的IP地址为本地地址“127.0.0.1”,使用的端口号为“12345”。客户端发送信息,服务器接收并返回信息,最后将服务器的接收消息和返回消息显示在两个TextView中。

由于Android中的线程不能改变UI界面,上述代码建立了两个Handler(ClientHandler和ServerHandler)分别用来处理客户端线程和服务器端线程产生的消息。

张飞:军师,为何要将Socket的创建和连接写在新的线程里,不能直接写在UI线程中吗?

孔明:这是因为Android 3.0以上已经不建议在Activity中添加耗时操作。因此Android 3.0以上的Socket通信都必须在一个新的线程里执行。

 

 

 

 

 


值得注意的是,需要在AndroidManifest.xml文件中添加访问网络的权限,如下所示:

<uses-permissionandroid:name="android.permission.INTERNET" />

运行程序,结果如图18-3所示:

图18-3 Socket通信

点击“开始通信”按钮,客户端就会向服务器端发送“服务器您好!”字符串信息,服务器成功接收到这个信息后就会立即返回“客户端您好”。

1.6. 玄德有话说  

张飞:军师,啥是FTP和WWW呀,和端口有什么关系,能不能讲得更通俗一点?。

孔明:二弟你可以这样想啦,有一天,你要去银行存钱,那个银行就可以想成是主机,银行里头就有相当多的窗口,当你一进大门时,在门口的服务人员就会问你说:“嗨!你好呀!你要做些什么事?”你跟他说:“我要存钱呀!”,服务员接着就会告诉你:“请前往三号窗口!那边的人员会帮您服务!”这些窗口就相当于计算机中的端口。

 

张飞:啥玩意儿,我咋运行Socket时报错

“java.net.SocketException: Permission denied (maybe missing INTERNETpermission)”啊?

孔明:这是由于没有在AndroidManifest.xml里添加权限

<uses-permissionandroid:name="android.permission.INTERNET" />。

 

张飞:军师,连接模拟器的IP地址是多少?

孔明:有的时候在Socket编程中需要Android模拟器的IP地址,连接本地Android模拟器时,其IP地址为10.0.2.2而不是127.0.0.1。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值