《Android智能穿戴设备开发指南》——第6章,第6.2节使用TCP协议传输数据

6.2 使用TCP协议传输数据
Android智能穿戴设备开发指南
TCP/IP通信协议是一种可靠的网络协议,能够在通信的两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。Java语言对TCP网络通信提供了良好的封装,通过Socket对象代表两端的通信端口,并通过Socket产生的IO流进行网络通信。本章将详细讲解Java应用中TCP编程的基本知识。

6.2.1 使用ServletSocket
在Java程序中,使用类ServerSocket接受其他通信实体的连接请求。对象ServerSocket的功能是监听来自客户端的Socket连接,如果没有连接则会一直处于等待状态。在类ServerSocket中包含了如下监听客户端连接请求的方法。

① Socket accept()。如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket,否则该方法将一直处于等待状态,线程也被阻塞。

② ServerSocket(int port)。用指定的端口PORT创建一个ServerSocket,该端口应该有一个有效的端口整数值:0~65 535。

③ ServerSocket(int port,int backlog)。增加一个用来改变连接队列长度的参数backlog。

④ ServerSocket(int port,int backlog,InetAddress localAddr)。在机器存在多个 IP地址的情况下,允许通过localAddr这个参数来指定将ServerSocket绑定到指定的IP地址。

当使用ServerSocket后,需要使用ServerSocket中的方法close()关闭该ServerSocket。在通常情况下,因为服务器不会只接受一个客户端请求,而是会不断地接受来自客户端的所有请求,所以可以通过循环来不断地调用ServerSocket中的方法accept(),见下面的代码。

//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss = new ServerSocket(30000);
//采用循环不断接收来自客户端的请求
while (true)
{
  //每当接收客户端Socket的请求,服务器端也对应产生一个Socket
  Socket s = ss.accept();
  //下面就可以使用Socket进行通信了
  ...
}

在上述代码中,创建的ServerSocket没有指定IP地址,该ServerSocket会绑定到本机默认的IP地址。在代码中使用30000作为该ServerSocket的端口号,通常推荐使用10000以上的端口,主要是为了避免与其他应用程序的通用端口冲突。

6.2.2 使用Socket
在客户端可以使用Socket的构造器实现和指定服务器的连接,在Socket中可以使用如下两个构造器。

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

② Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort)。

创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口号,适用于本地主机有多个IP地址的情形。

在使用上述构造器指定远程主机时,既可使用InetAddress来指定,也可以使用String对象指定,在Java中通常使用String对象指定远程IP,如192.168.2.23。当本地主机只有一个IP地址时,建议使用第一个方法,因为这样更简单。例如下面的代码。

//创建连接到本机、30000端口的Socket
Socket s = new Socket("127.0.0.1" , 30000);

当程序执行上述代码后会连接到指定服务器,让服务器端的ServerSocket的方法accept()向下执行,于是服务器端和客户端就产生一对互相连接的Socket。上述代码连接到“远程主机”的IP地址是127.0.0.1,此IP地址总是代表本级的IP地址。因为笔者示例程序的服务器端、客户端都是在本机运行,所以Socket连接到远程主机的IP地址使用127.0.0.1。

当客户端、服务器端产生对应的Socket之后,程序无需再区分服务器端和客户端,而是通过各自的Socket进行通信。在Socket中提供如下两个方法获取输入流和输出流。

① InputStream getInputStream()。返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。

② OutputStream getOutputStream()。返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。

例如,下面是一段TCP协议的服务器端程序。

源码路径:daima\6\tcpudp\src\Server.java。

import java.net.*;
import java.io.*;
public class Server
{
  public static void main(String[] args)
    throws IOException
  {
    //创建一个ServerSocket,用于监听客户端Socket的连接请求
    ServerSocket ss = new ServerSocket(30000);
    //采用循环不断接收来自客户端的请求
    while (true)
    {
      //每当接收到客户端Socket的请求,服务器端也对应产生一个Socket
      Socket s = ss.accept();
      //将Socket对应的输出流包装成PrintStream
      PrintStream ps = new PrintStream(s.getOutputStream());
      //进行普通I/O操作
      ps.println("圣诞快乐!");
      //关闭输出流,关闭Socket
      ps.close();
      s.close();
    }
  }
}

通过上述代码建立了ServerSocket监听,并且使用Socket获取了输出流,所以执行后不会显示任何信息。

而下面是一段TCP协议的客户端程序。

源码路径:daima\6\tcpudp\src\Client.java。

import java.net.*;
import java.io.*;
public class Client
{
  public static void main(String[] args)
    throws IOException
  {
    Socket socket = new Socket("127.0.0.1" , 30000);
    //将Socket对应的输入流包装成BufferedReader
    BufferedReader br = new BufferedReader(
      new InputStreamReader(socket.getInputStream()));
    //进行普通I/O操作
    String line = br.readLine();
    System.out.println("来自服务器的数据:" + line);
    //关闭输入流、socket
    br.close();
    socket.close();
  }
}

上述代码使用Socket建立了与指定IP、指定端口的连接,并使用Socket获取输入流读取数据。执行后的效果如图6-1所示。

由此可见,一旦使用ServerSocket和Socket建立网络连接之后,程序通过网络通信与普通I/O并没有太大的区别。如果先运行上面程序中的Server类,将看到服务器一直处于等待状态,因为服务器使用了死循环来接收来自客户端的请求;再运行Client类,将可看到程序输出“来自服务器的数据:圣诞快乐!”这表明客户端和服务器端通信成功。上述代码为了突出通过ServerSocket和Socket建立连接,并通过底层I/O流进行通信的主题,程序没有进行异常处理,也没有使用finally块来关闭资源。

6.2.3 TCP中的多线程
在6.2.2节的实例中,Server和Client只是进行了简单的通信操作,当服务器接收到客户端连接之后,向客户端输出一个字符串,而客户端也只是读取服务器的字符串后就退出了。在实际应用中,客户端可能需要和服务器保持长时间的通信,即服务器需要不断地读取客户端的数据,并向客户端写入数据,客户端也需要不断地读取服务器的数据,并向服务器写入数据。

当使用readLine()方法读取数据时,如果在该方法成功返回之前线程被阻塞,则程序无法继续执行。所以此服务器很有必要为每个Socket单独启动一条线程,每条线程负责与一个客户端进行通信。另外,因为客户端读取服务器数据的线程同样会被阻塞,所以系统应该单独启动一条线程,该线程专门负责读取服务器的数据。

假设要开发一个聊天室程序,在服务器端应该包含多条线程,其中每个Socket对应一条线程,该线程负责读取Socket对应输入流的数据(从客户端发送过来的数据),并将读到的数据向每个Socket输出流发送一遍(将一个客户端发送的数据“广播”给其他客户端),因此,需要在服务器端使用List来保存所有的Socket。在具体实现时,为服务器提供了如下两个类。

① 创建ServerSocket监听的主类。

② 处理每个Socket通信的线程类。

接下来介绍具体实现流程,首先看一段代码。

源码路径:daima\6\tcpudp\src\liao\server\IServer.java。

package liao.server;
import java.net.*;
import java.io.*;
import java.util.*;
public class IServer
{
  //定义保存所有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 Serverxian(s)).start();
    }
  }
}

在上述代码中,服务器端只负责接收客户端Socket的连接请求,每当客户端Socket连接到该ServerSocket之后,程序将对应Socket加入 socketList集合中保存,并为该Socket启动一条线程,该线程负责处理该Socket所有的通信任务。

然后看服务器端线程类文件的主要代码。

源码路径:daima\6\tcpudp\src\liao\server\Serverxian.java。

//负责处理每个线程通信的线程类
public class Serverxian implements Runnable
{
  //定义当前线程所处理的Socket
  Socket s = null;
  //该线程所处理的Socket所对应的输入流
  BufferedReader br = null;
  public Serverxian(Socket s)
    throws IOException
  {
    this.s = s;
    //初始化该Socket对应的输入流
    br = new BufferedReader(new InputStreamReader(s.getInputStream()));
  }
  public void run()
  {
    try
    {
      String content = null;
      //采用循环不断从Socket中读取客户端发送过来的数据
      while ((content = readFromClient()) != null)
      {
        //遍历socketList中的每个Socket,将读到的内容向每个Socket发送一次
        for (Socket s : IServer.socketList)
        {
          PrintStream ps = new PrintStream(s.getOutputStream());
          ps.println(content);
        }
      }
    }
    catch (IOException e)
    {
      //e.printStackTrace();
    }
  }
  //定义读取客户端数据的方法
  private String readFromClient()
  {
    try
    {
      return br.readLine();  
    }
    //如果捕捉到异常,表明该Socket对应的客户端已经关闭
    catch (IOException e)
    {
      //删除该Socket。
      IServer.socketList.remove(s);
    }
    return null;
  }
}

在上述代码中,服务器端线程类会不断读取客户端数据,在获取时使用方法readFromClient()来读取客户端数据。如果读取数据过程中捕获到 IOException异常,则说明此Socket对应的客户端Socket出现了问题,程序就会将此Socket从socketList中删除。当服务器线程读到客户端数据之后会遍历整个socketList集合,并将该数据向socketList集合中的每个Socket发送一次,该服务器线程将把从Socket中读到的数据向socketList中的每个Socket转发一次。

接下来开始客户端的编码工作,在本应用的每个客户端应该包含如下2条线程。

① 第一条:功能是读取用户的键盘输入,并将用户输入的数据写入Socket对应的输出流。

② 第二条:功能是读取Socket对应输入流中的数据(从服务器发送过来的数据),并将这些数据打印输出。其中负责读取用户键盘输入的线程由Myclient负责,也就是由程序的主线程负责。

客户端主程序文件的主要代码如下。

源码路径:daima\6\tcpudp\src\liao\server\Iclient.java。

public class IClient
{
  public static void main(String[] args)
    throws IOException
  {
    Socket s = s = new Socket("127.0.0.1" , 30000);
    //客户端启动ClientThread线程不断读取来自服务器的数据
    new Thread(new ClientThread(s)).start();
    //获取该Socket对应的输出流
    PrintStream ps = new PrintStream(s.getOutputStream());
    String line = null;
    //不断读取键盘输入
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    while ((line = br.readLine()) != null)
    {
      //将用户的键盘输入内容写入Socket对应的输出流
      ps.println(line);
    }
  }
}

在上述代码中,当线程读到用户键盘输入的内容后,会将用户键盘输入的内容写入该Socket对应的输出流。当主线程使用Socket连接到服务器之后,会启动ClientThread来处理该线程的Socket通信。

最后编写客户端的线程处理文件,此线程负责读取Socket输入流中的内容,并将这些内容在控制台打印出来。具体代码(见源程序daima6tcpudpsrcliaoserverClientxian.java)如下。

public class Clientxian implements Runnable
{
  //该线程负责处理的Socket
  private Socket s;
  //该线程所处理的Socket对应的输入流
  BufferedReader br = null;
  public Clientxian(Socket s)
    throws IOException
  {
    this.s = s;
    br = new BufferedReader(
      new InputStreamReader(s.getInputStream()));
  }
  public void run()
  {
    try
    {
      String content = null;
      //不断读取Socket输入流中的内容,并将这些内容打印输出
      while ((content = br.readLine()) != null)
      {
        System.out.println(content);
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  }
}

上述代码能够不断获取Socket输入流中的内容,当获取Socket输入流中的内容后,直接将这些内容打印在控制台。先运行上面程序中的类IServer,该类运行后作为本应用的服务器,不会看到任何输出。接着可以运行多个IClient——相当于启动多个聊天室客户端登录该服务器,此时可以看到,在任何一个客户端通过键盘输入一些内容后,按回车键,所有客户端(包括自己)都会在控制台收到它刚刚输入的内容,这就简单地实现了一个聊天室的功能。

6.2.4 实现非阻塞Socket通信
在Java应用程序中,可以使用NIO API来开发高性能网络服务器。当程序执行输入、输出操作后,在这些操作返回之前会一直阻塞该线程,服务器必须为每个客户端都提供一条独立的线程进行处理。这说明前面的程序是基于阻塞式API的,当服务器需要同时处理大量客户端时,这种做法会降低性能。

在Java应用程序中可以用NIO API让服务器使用一个或有限几个线程来同时处理连接到服务器上的所有客户端。在Java的NIO中,为非阻塞式的Socket通信提供了下面的特殊类。

① Selector。它是SelectableChannel对象的多路复用器,所有希望采用非阻塞方式进行通信的Channel都应该注册到 Selector对象。可通过调用此类的静态open()方法来创建Selector实例,该方法将使用系统默认的Selector来返回新的Selector。Selector可以同时监控多个SelectableChannel的I/O状况,是非阻塞I/O的核心。一个Selector实例有如下3个SelectionKey的集合。

a.所有SelectionKey集合。它代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。

b.被选择的SelectionKey集合。它代表了所有可通过select()方法监测到、需要进行I/O处理的Channel,这个集合可以通过selectedKeys()返回。

c.被取消的SelectionKey集合。它代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问该集合。

除此之外,Selector还提供了如下和select()相关的方法。

a.int select()。监控所有注册的Channel,当它们中间有需要处理的I/O操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。

b.int select(long timeout)。可以设置超时时长的select()操作。

c.int selectNow()。执行一个立即返回的select()操作,相对于无参数的select()方法而言,该方法不会阻塞线程。

Selector wakeup()。使一个还未返回的select()方法立刻返回。

② SelectableChannel。它代表可以支持非阻塞I/O操作的Channel对象,可以将其注册到Selector上,这种注册的关系由SelectionKey实例表示。在Selector对象中,可以使用select()方法设置允许应用程序同时监控多个I/O Channel。Java程序可调用SelectableChannel中的register()方法将其注册到指定Selector上,当该Selector上某些SelectableChannel上有需要处理的I/O操作时,程序可以调用Selector实例的select()方法获取它们的数量,并通过selectedKeys()方法返回它们对应的SelectKey集合。这个集合的作用巨大,因为通过该集合就可以获取所有需要处理I/O操作的SelectableChannel集。

对象SelectableChannel支持阻塞和非阻塞两种模式,其中所有Channel默认都是阻塞模式,我们必须使用非阻塞式模式才可以利用非阻塞I/O操作。

在SelectableChannel中提供了如下两个方法来设置和返回该Channel的模式状态。

a.SelectableChannel configureBlocking(boolean block)。设置是否采用阻塞模式。

b.boolean isBlocking()。返回该Channel是否是阻塞模式。

不同的SelectableChannel所支持的操作不一样,如ServerSocketChannel代表一个ServerSocket,它就只支持OP_ACCEPT操作。在SelectableChannel中提供了如下方法来返回它支持的所有操作。

int validOps():返回一个bit mask,表示这个Channel上支持的I/O操作。

除此之外,SelectableChannel还提供了如下方法获取它的注册状态。

① boolean isRegistered()。返回该Channel是否已注册在一个或多个Selector上。

② SelectionKey keyFor(Selector sel)。返回该Channel和sel Selector之间的注册关系,如果不存在注册关系,则返回null。

③ SelectionKey。该对象代表SelectableChannel和Selector之间的注册关系。

④ ServerSocketChannel。支持非阻塞操作,对应于java.net.ServerSocket这个类,提供了TCP协议I/O接口,只支持OP_ACCEPT操作。该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。

⑤ SocketChannel。支持非阻塞操作,对应于java.net.Socket这个类,提供了TCP协议I/O接口,支持OP_CONNECT、OP_READ和OP_WRITE操作。这个类还实现了ByteChannel接口、ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通过SocketChannel来读写ByteBuffer对象。

服务器上所有Channel都需要向Selector注册,包括ServerSocketChannel和SocketChannel。该Selector则负责监视这些Socket的I/O状态,当其中任意一个或多个Channel具有可用的I/O操作时,该Selector的select()方法将会返回大于0的整数,该整数值就表示该Selector上有多少个Channel具有可用的I/O操作,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合。正是通过Selector才使得服务器端只需要不断地调用Selector实例的select()方法,这样就可以知道当前所有Channel是否有需要处理的I/O操作。当Selector上注册的所有Channel都没有需要处理的I/O操作时,将会阻塞select()方法,此时调用该方法的线程被阻塞。

我们继续以聊天室为例,讲解非阻塞Socket通信在Java应用项目中的实现过程。我们的目标是,在服务器端使用循环不断获取Selector的select()方法返回值,当该返回值大于0时就处理该Selector上被选择SelectionKey所对应的Channel。在具体实现时,服务器端使用ServerSocketChannel来监听客户端的连接请求,程序先调用它的socket()方法获得关联ServerSocket对象,再用该ServerSocket对象绑定到来指定监听IP和端口。最后在服务器端调用Selector的select()方法来监听所有Channel上的I/O操作。

接下来开始具体编码,其中服务器端的主要代码如下。

源码路径:daima\6\tcpudp\src\feizu\feizuServer.java。

public class feizuServer
{
  //用于检测所有Channel状态的Selector
  private Selector selector = null;
  //定义实现编码、解码的字符集对象
  private Charset charset = Charset.forName("UTF-8");
  public void init()throws IOException
  {
    selector = Selector.open();
    //通过open方法来打开一个未绑定的ServerSocketChannel实例
    ServerSocketChannel server = ServerSocketChannel.open();
    InetSocketAddress isa = new InetSocketAddress(
      "127.0.0.1", 30000);
    //将该ServerSocketChannel绑定到指定IP地址
    server.socket().bind(isa);
    //设置ServerSocket以非阻塞方式工作
    server.configureBlocking(false);
    //将server注册到指定Selector对象
    server.register(selector, SelectionKey.OP_ACCEPT);
    while (selector.select() > 0)
    {
      //依次处理selector上每个已选择的SelectionKey
      for (SelectionKey sk : selector.selectedKeys())
      {
        //从selector上的已选择Key集中删除正在处理的SelectionKey
        selector.selectedKeys().remove(sk);
        //如果sk对应的通道包含客户端的连接请求
        if (sk.isAcceptable())
        {
          //调用accept方法接收连接,产生服务器端对应的SocketChannel
          SocketChannel sc = server.accept();
          //设置采用非阻塞模式
          sc.configureBlocking(false);
          //将该SocketChannel也注册到selector
          sc.register(selector, SelectionKey.OP_READ);
          //将sk对应的Channel设置成准备接收其他请求
          sk.interestOps(SelectionKey.OP_ACCEPT);
        }
        //如果sk对应的通道有数据需要读取
        if (sk.isReadable())
        {
          //获取该SelectionKey对应的Channel,该Channel中有可读的数据
          SocketChannel sc = (SocketChannel)sk.channel();
          //定义准备执行读取数据的ByteBuffer
          ByteBuffer buff = ByteBuffer.allocate(1024);
          String content = "";
          //开始读取数据
          try
          {
            while(sc.read(buff) > 0)
            {
              buff.flip();
              content += charset.decode(buff);
            }
            //打印从该sk对应的Channel里读取到的数据
            System.out.println("=====" + content);
            //将sk对应的Channel设置成准备下一次读取
            sk.interestOps(SelectionKey.OP_READ);
          }
          //如果捕捉到该sk对应的Channel出现了异常,即表明该Channel
          //对应的Client出现了问题,所以从Selector中取消sk的注册
          catch (IOException ex)
          {
            //从Selector中删除指定的SelectionKey
            sk.cancel();
            if (sk.channel() != null)
            {
              sk.channel().close();
            }
          }
          //如果content的长度大于0,则聊天信息不为空
          if (content.length() > 0)
          {
            //遍历该selector里注册的所有SelectKey
            for (SelectionKey key : selector.keys())
            {
              //获取该key对应的Channel
              Channel targetChannel = key.channel();
              //如果该channel是SocketChannel对象
              if (targetChannel instanceof SocketChannel)
              {
                //将读到的内容写入该Channel
                SocketChannel dest = (SocketChannel)targetChannel;
                dest.write(charset.encode(content));
              }
            }
          }
        }
      }
    }
  }

  public static void main(String[] args)
    throws IOException
  {
    new feizuServer().init();
  }
}

通过上述代码,在启动时马上建立一个可监听连接请求的ServerSocketChannel,并将该Channel注册到指定的Selector,接着程序直接采用循环不断监控Selector对象的select()方法返回值,当该返回值大于0时处理该Selector上所有被选择的 SelectionKey。在处理指定SelectionKey之后立即从该Selector中的被选择的SelectionKey集合中删除该SelectionKey。服务器端的Selector仅需要监听连接和读数据这两种操作,在处理连接操作时只需将接受连接后产生的SocketChannel注册到指定Selector对象即可。当处理读数据操作后,系统先从该Socket中读取数据,再将数据写入Selector上注册的所有Channel。

接下来开始编写客户端的代码,本应用的客户端程序需要如下两个线程。

① 负责读取用户的键盘输入,并将输入的内容写入SocketChannel。

② 不断查询Selector对象的select()方法的返回值。

客户端的主要代码如下。

源码路径:daima\6\tcpudp\src\feizu\feizuClient.java。

public class feizuClient{
  //定义检测SocketChannel的Selector对象
  private Selector selector = null;
  //定义处理编码和解码的字符集
  private Charset charset = Charset.forName("UTF-8");
  //客户端SocketChannel
  private SocketChannel sc = null;
  public void init()throws IOException
  {
    selector = Selector.open();
    InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
    //调用open静态方法创建连接到指定主机的SocketChannel
    sc = SocketChannel.open(isa);
    //设置该sc以非阻塞方式工作
    sc.configureBlocking(false);
    //将SocketChannel对象注册到指定Selector
    sc.register(selector, SelectionKey.OP_READ);
    //启动读取服务器端数据的线程
    new ClientThread().start();
    //创建键盘输入流
    Scanner scan = new Scanner(System.in);
    while (scan.hasNextLine())
    {
      //读取键盘输入
      String line = scan.nextLine();
      //将键盘输入的内容输出到SocketChannel
      sc.write(charset.encode(line));
    }
  }
  //定义读取服务器数据的线程
  private class ClientThread extends Thread
  {
    public void run()
    {
      try
      {
        while (selector.select() > 0)
        {
          //遍历每个有可用I/O操作Channel对应的SelectionKey
          for (SelectionKey sk : selector.selectedKeys())
          {
            //删除正在处理的SelectionKey
            selector.selectedKeys().remove(sk);
            //如果该SelectionKey对应的Channel中有可读的数据
            if (sk.isReadable())
            {
              //使用NIO读取Channel中的数据
              SocketChannel sc = (SocketChannel)sk.channel();
              ByteBuffer buff = ByteBuffer.allocate(1024);
              String content = "";
              while(sc.read(buff) > 0)
              {
                sc.read(buff);
                buff.flip();
                content += charset.decode(buff);
              }
              //打印输出读取的内容
              System.out.println("聊天信息:" + content);
              //为下一次读取做准备
              sk.interestOps(SelectionKey.OP_READ);
            }
          }
        }
      }
      catch (IOException ex)
      {
        ex.printStackTrace();
      }
    }
  }
  public static void main(String[] args)
    throws IOException
  {
    new feizuClient().init();
  }

上述客户端代码只有一条SocketChannel,当此SocketChannel注册到指定的Selector后,程序会启动另一条线程来监测该Selector。

在使用NIO来实现服务器时,甚至无须使用ArrayList来保存服务器中所有SocketChannel,因为所有的SocketChannel都需要注册到指定的Selector对象。除此之外,当客户端关闭时会导致服务器对应的Channel也抛出异常,而且本程序只有一条线程,如果该异常得不到处理将会导致整个服务器退出,所以程序捕捉了这种异常,并在处理异常时从Selector删除异常Channel的注册。

时间: 2024-11-08 17:04:49

《Android智能穿戴设备开发指南》——第6章,第6.2节使用TCP协议传输数据的相关文章

《Android智能穿戴设备开发指南》——第6章,第6.4节使用Socket发送求救信号

6.4 使用Socket发送求救信号Android智能穿戴设备开发指南通过本章前面内容的学习,已经了解了Java应用中Socket网络编程的基本知识.在Android平台中,可以使用相同的方法用Socket实现数据传输功能.本节将通过一个具体实例的实现过程,来讲解在Android穿戴设备中使用Socket发送求救信号的基本方法. 实例6-1 使用Socket发送求救信号,源码路径:daima6socket. 本实例的具体实现流程如下. ① 首先实现服务器端,使用Eclipse新建一个名为andr

《Android智能穿戴设备开发指南》——第6章,第6.3节使用UDP协议传递数据

6.3 使用UDP协议传递数据 Android智能穿戴设备开发指南 Java为我们提供了DatagramSocket对象作为基于UDP协议的Socket,可以使用DatagramPacket代表DatagramSocket发送或接收的数据报.本节将详细讲解使用UDP协议传递数据的内容. 6.3.1 使用DatagramSocket进行数据交互 DatagramSocket本身只是码头,不能产生I/O流,其唯一的功能是接收和发送数据报.Java语言使用DatagramPacket代表数据报,Dat

《Android智能穿戴设备开发指南》——第6章,第6.1节Socket编程基础

6.1 Socket编程基础 Android智能穿戴设备开发指南 网络编程中有两个主要问题,一个是如何准确地定位网络中的一台或多台主机,另一个就是找到主机后如何可靠.高效地进行数据传输.在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,并通过IP地址可以唯一地确定Internet上的一台主机.TCP层提供面向应用的可靠(TCP)的或非可靠(UDP)的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的.目前较为流行的网络编程模型是客户机/服务器(C/S)结构

《Android智能穿戴设备开发指南》——导读

目 录前言 第1章 Android开发技术基础第2章 建立Android应用开发环境第3章 获取并编译源代码第4章 Android技术核心框架第5章 Android中HTTP网络通信第6章 Android中使用Socket实现数据通信 6.1 Socket编程基础 6.2 使用TCP协议传输数据 6.3 使用UDP协议传递数据6.4 使用Socket发送求救信号第7章 Android中下载远程数据第8章 Android中上传数据第9章 使用URL处理数据第10章 处理XML数据第11章 在穿戴设

《智能路由器开发指南》——导读

前 言 OpenWrt成功的秘密 可以实现路由器功能的开源软件很多,为什么只有OpenWrt成功了?OpenWrt软件成功的关键在于3个方面:领导者.基础设施以及实现软件的技术.通常领导者是最重要的,因为领导者决定着社区的规则和技术方向,但是每个人都是独特的而且是无法复制的,因此通常无法借鉴.基础设施和实现软件的技术则是可以借鉴的. OpenWrt社区采用六大基础设施工具支撑整个社区的运转,这六大基础设施工具分别是代码管理工具Git.邮件列表.自动构建工具buildbot.文档管理工具WiKi.

Google Web App开发指南第三章:案例研究

旅程计划应用(Wayfindit: Trip Planner App) 在大多数情况下,Wayfindit的应用必须有很好的易用性.旅行是一件很复杂的事情,不管是商业旅行还是休假旅行,一个顺利的旅程要求从家门到目的都没有意外之忧.Wayfindit的应用要能给旅行者提供所需信息,并且要快而准确.这意味着它需要一个最小的.直观的.响应式界面,能在前端提供有关内容的重要信息--HTML5的地理感知和离线存储特性实现. 一个完美的袖珍指南 它就装在你的口袋里或者包里,即时提供信息.它拥有本地存储和地理

Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 这一章很多,但是很有趣,也是这书的最后一章知识点了,我现在还在考虑要不要写这个拼图和2048的案例,在此之前,我们先来玩玩Android5.X的新特性吧!

网络医疗的进步让智能穿戴设备找到市场新蓝海

随着物联网.大数据分析技术的快速发展,能够感知与撷取数据的智能穿戴设备需求日趋明显,但由于智能穿戴设备包括智能眼镜.智能手表及手环等,始终无法在消费市场掀起如同智能手机一样的浪潮,设法转进到其他利基市场,便成为智能穿戴设备未来的重要出路. 智能穿戴设备正开始普及于每个人的生活中(Wikipedia). 随着物联网.大数据分析技术的快速发展,能够感知与撷取数据的智能穿戴设备需求日趋明显,但由于智能穿戴设备包括智能眼镜.智能手表及手环等,始终无法在消费市场掀起如同智能手机一样的浪潮,设法转进到其他利

2014年上半年智能穿戴设备购买攻略

相关新闻主流智能穿戴设备推荐一 智能穿戴设备的前景[TechWeb报道]早在几年前我们身上携带的最多的还是功能手机,传统的按键布局,死板的功能应用,让我们只奢望能顺利打电话.收发短信.随手拍几张照片就心满意足了,而随着科技的进步,厂商对市场的需求分析.自身产品研发能力的提高与消费者对数码类产品的消费欲望的提高,智能手机的出现几乎完全取代了功能机,除开最基本的功能外,其强大的功能扩展性.易用性.前瞻性使它注定会发展的更好更完善,智能手机也可以看做是一个智能移动终端设备,它所实现的一切都基于五花八门