神奇好望角 The Magical Cape of Good Hope

庸人不必自扰,智者何需千虑?
posts - 26, comments - 50, trackbacks - 0, articles - 11
  BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

JDK 提供了对 TCP(Transmission Control Protocol,传输控制协议)和 UDP(User Datagram Protocol,用户数据报协议)这两个数据传输协议的支持。本文开始探讨 TCP。

TCP 基础知识

在“服务器-客户端”这种架构中,服务器和客户端各自维护一个端点,两个端点需要通过网络进行数据交换。TCP 为这种需求提供了一种可靠的流式连接,流式的意思是传出和收到的数据都是连续的字节,没有对数据量进行大小限制。一个端点由 IP 地址和端口构成(专业术语为“元组 {IP 地址, 端口}”)。这样,一个连接就可以由元组 {本地地址, 本地端口, 远程地址, 远程端口} 来表示。

连接过程

在 TCP 编程接口中,端点体现为 TCP 套接字。共有两种 TCP 套接字:主动和被动,“被动”状态也常被称为“侦听”状态。服务器和客户端利用套接字进行连接的过程如下:

  1. 服务器创建一个被动套接字,开始循环侦听客户端的连接。
  2. 客户端创建一个主动套接字,连接服务器。
  3. 服务器接受客户端的连接,并创建一个代表该连接的主动套接字。
  4. 服务器和客户端通过步骤 2 和 3 中创建的两个主动套接字进行数据传输。

下面是连接过程的图解:

TCP 连接
TCP 连接

一个简单的 TCP 服务器

JDK 提供了 ServerSocket 类来代表 TCP 服务器的被动套接字。下面的代码演示了一个简单的 TCP 服务器(多线程阻塞模式),它不断侦听并接受客户端的连接,然后将客户端发送过来的文本按行读取,全文转换为大写后返回给客户端,直到客户端发送文本行 bye

public class TcpServer implements Runnable {
    private ServerSocket serverSocket;

    public TcpServer(int port) throws IOException {
        // 创建绑定到某个端口的 TCP 服务器被动套接字。
        serverSocket = new ServerSocket(port);
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 以阻塞的方式接受一个客户端连接,返回代表该连接的主动套接字。
                Socket socket = serverSocket.accept();
                // 在新线程中处理客户端连接。
                new Thread(new ClientHandler(socket)).start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = Objects.requireNonNull(socket);
    }

    @Override
    public void run() {
        try (Socket s = socket) {  // 减少代码量的花招……
            // 包装套接字的输入流以读取客户端发送的文本行。
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    s.getInputStream(), StandardCharsets.UTF_8));
            // 包装套接字的输出流以向客户端发送转换结果。
            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                    s.getOutputStream(), StandardCharsets.UTF_8), true);

            String line = null;
            while ((line = in.readLine()) != null) {
                if (line.equals("bye")) {
                    break;
                }

                // 将转换结果输出给客户端。
                out.println(line.toUpperCase(Locale.ENGLISH));
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
    

阻塞模式的编程方式简单,但存在性能问题,因为服务器线程会卡死在接受客户端的 accept() 方法上,不能有效利用资源。套接字支持非阻塞模式,现在暂时略过。

一个简单的 TCP 客户端

JDK 提供了 Socket 类来代表 TCP 客户端的主动套接字。下面的代码演示了上述服务器的客户端:

public class TcpClient implements Runnable {
    private Socket socket;

    public TcpClient(String host, int port) throws IOException {
        // 创建连接到服务器的套接字。
        socket = new Socket(host, port);
    }

    @Override
    public void run() {
        try (Socket s = socket) {  // 再次减少代码量……
            // 包装套接字的输出流以向服务器发送文本行。
            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                    s.getOutputStream(), StandardCharsets.UTF_8), true);
            // 包装套接字的输入流以读取服务器返回的文本行。
            BufferedReader in = new BufferedReader(new InputStreamReader(
                    s.getInputStream(), StandardCharsets.UTF_8));

            Console console = System.console();
            String line = null;
            while ((line = console.readLine()) != null) {
                if (line.equals("bye")) {
                    break;
                }

                // 将文本行发送给服务器。
                out.println(line);
                // 打印服务器返回的文本行。
                console.writer().println(in.readLine());
            }

            // 通知服务器关闭连接。
            out.println("bye");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
    

从 JDK 文档可以看到,ServerSocketSocket 在初始化的时候,可以设定一些参数,还支持延迟绑定。这些东西对性能和行为都有所影响。下一篇文章将详解这两个类的初始化。


评论

# re: Java 网络编程从菜鸟到叫兽 2:TCP 和套接字入门  回复  更多评论   

2012-01-05 17:13 by 何杨
标题起得不错。

# re: Java 网络编程从菜鸟到叫兽 2:TCP 和套接字入门[未登录]  回复  更多评论   

2012-01-05 21:06 by song
还不够菜 嘿嘿

# re: Java 网络编程从菜鸟到叫兽 2:TCP 和套接字入门  回复  更多评论   

2012-01-10 14:35 by #
上面对节省代码地方,一直未搞明白!

# re: Java 网络编程从菜鸟到叫兽 2:TCP 和套接字入门  回复  更多评论   

2012-01-10 15:38 by 蜀山兆孨龘
@#
建议你先看看 Java SE 7 的新功能“带资源的 try”,就容易明白了。直接写 try (socket) 是语法错误,因为 try 后面的括号里必须是赋值表达式,所以才引入临时变量 s 暗度陈仓。

# re: Java 网络编程从菜鸟到叫兽 2:TCP 和套接字入门  回复  更多评论   

2012-01-10 16:09 by 开启
学习

只有注册用户登录后才能发表评论。


网站导航: