TCP 编程 Java 为 TCP 协议提供了两个类:ServerSocket
和 Socket
类。 编程的步骤大致如下:
创建服务器套接字 ServerSocket
,并将其绑定到特定地址(IP 地址和端口号)。
调用 accept()
方法,侦听并接受到此套接字的连接。
创建客户端套接字 Socket
,并将其连接到指定的地址。
连接建立后,Socket
可以通过 getOutputStream()
获取输出流,向服务端发送数据。同时,ServerSocket
则可以通过 getInputStream()
获取输入流,来读取客户端的数据。
类似的,ServerSocket
可以通过 getOutputStream()
获取输出流,向客户端发送数据。Socket
也可以通过 getInputStream()
获取输入流,来读取服务器的数据。
最后,当然还需要关闭连接。
服务器端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.ServerSocket;import java.net.Socket;public class TCPServer { public static void main (String[] args) { final int PORT = 12345 ; ServerSocket server = null ; try { server = new ServerSocket(PORT); while (true ) { Socket client = server.accept(); try (BufferedReader in = new BufferedReader( new InputStreamReader(client.getInputStream())); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(client.getOutputStream()));) { String data; while ((data = in.readLine()) != null ) { System.out.println("from client[" + client.getRemoteSocketAddress().toString() + "] -> " + data); out.write(data + " received" ); out.newLine(); out.flush(); } } } } catch (IOException e) { e.printStackTrace(); } finally { if (server != null ) { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.Socket;import java.net.UnknownHostException;public class TCPClient { public static void main (String[] args) { final String IP = "127.0.0.1" ; final int PORT = 12345 ; Socket client = null ; try { client = new Socket(IP, PORT); try (BufferedWriter out = new BufferedWriter( new OutputStreamWriter(client.getOutputStream())); BufferedReader in = new BufferedReader( new InputStreamReader(client.getInputStream()));) { out.write(String.valueOf(Math.random())); out.newLine(); out.flush(); String data; while ((data = in.readLine()) != null ) { System.out.println("from server[" + client.getRemoteSocketAddress().toString() + "] -> " + data); } } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
并发 以上的代码虽然允许多个客户端与服务器建立连接,但是服务端处理客户端的连接请求是同步进行的。也就是说,服务器每次接收到来自客户端的连接请求后,都要先跟当前的客户端通信完成后,才能接着处理下一个客户端的连接请求。 在实际工作中,一个服务器往往需要同时处理来自多个客户端的连接请求。 这时候就需要引入多线程,一旦监听到新的客户端请求连接,就应该启动(或者从线程池中获取)一个新的线程来响应请求。
新增线程类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.net.Socket;public class TCPThread implements Runnable { private Socket client; public TCPThread (Socket client) { this .client = client; } @Override public void run () { try (BufferedReader in = new BufferedReader(new InputStreamReader( client.getInputStream())); BufferedWriter out = new BufferedWriter(new OutputStreamWriter( client.getOutputStream()));) { String data; while ((data = in.readLine()) != null ) { System.out.println("from client[" + client.getRemoteSocketAddress().toString() + "] -> " + data); out.write(data + " received" ); out.newLine(); out.flush(); } } catch (IOException e) { e.printStackTrace(); } } }
服务器端修改后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import java.io.IOException;import java.net.ServerSocket;import java.net.Socket;public class TCPServer { public static void main (String[] args) { final int PORT = 12345 ; ServerSocket server = null ; try { server = new ServerSocket(PORT); while (true ) { Socket client = server.accept(); new Thread(new TCPThread(client)).start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (server != null ) { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
UDP 编程 Java 为 UDP 协议提供了:DatagramSocket
和 DatagramPacket
类。 编程的步骤大致如下:
创建接收端的数据报套接字 DatagramSocket
,并将其绑定到指定的地址。
监听端口,通过 DatagramSocket
的 receive()
方法接收数据报文 DatagramPacket
。
创建发送端的数据报套接字,然后通过 send()
方法来发送 DatagramPacket
。
最后,关闭连接。
服务器段代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.SocketException;public class UDPServer { public static void main (String[] args) { final int PORT = 54321 ; DatagramSocket server = null ; try { server = new DatagramSocket(PORT); byte buf[] = new byte [1024 ]; DatagramPacket data = new DatagramPacket(buf, buf.length); while (true ) { server.receive(data); System.out.println("from client[" + data.getAddress().toString() + ":" + data.getPort() + "] -> " + new String(data.getData())); } } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (server != null ) { server.close(); } } } }
客户端代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.net.SocketException;public class UDPClient { public static void main (String[] args) { final int PORT = 54321 ; DatagramSocket client = null ; try { client = new DatagramSocket(); InetAddress address = InetAddress.getByName("localhost" ); byte buf[] = String.valueOf(Math.random()).getBytes(); DatagramPacket data = new DatagramPacket(buf, buf.length, address, PORT); client.send(data); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (client != null ) { client.close(); } } } }
总结
计算机网络的真正“用户”是运行在主机上的应用程序。
信息 是由程序创建的数据(字节序列)。
协议 是相互通信的应用程序间达成的一种约定,它规定了数据交换的方式。设计一组协议往往是为了在特定条件下解决某一类问题。
TCP/IP 是互联网使用的协议族 ,它主要包含:IP 协议(网络层)、 TCP 协议(传输层) 和 UDP 协议(传输层)。
一个互联网地址(InetAddress) 代表了一台主机与底层通信信道的连接,简单来说就是一个网络接口。举例来说,IP 地址相当于一栋大楼的门牌号,而端口号则是这个大楼里面的房间号。
套接字(Socket) 是应用程序和传输层之间的一个抽象层,应用程序通过它来发送和接收数据,与网络中其他应用程序进行通信。套接字在类型上,又分为:流套接字和数据报套接字。
TCP 是面向连接的协议,好比打电话 ,通信两端需要建立连接。
而 UDP 不是连接型协议,更像是邮政服务 ,你只需要为每个包裹指定目标地址即可。 因此,我们可以把 UDP 套接字看成是一个信箱,可以向不同地址发送信息,也可以接收从其它地址发过来的信息。UDP 套接字会尽可能(best-effort)地传输信息,但是并不保证一定送到目的地,也不保证严格按照顺序来发送。因此,使用 UDP 要做好信息丢失和重排的准备。
还有一点需要注意的:TCP 是面向(字节)流的,而 UDP 则是面向数据报的 。 另一种更复杂的说法是:UDP 每次收发的报文都会保留消息边界 。 这怎么理解呢? 在我看来,保留消息边界,就是要划清界限的意思,也就是说,每个数据报都会和其它的数据报划清界限。换言之,数据报之间相互独立。 这个特性,决定了 UDP 使用的数据报套接字时,每一次调用 receive()
方法只能接收一次 send()
方法发送的数据报文。 再回过头来看 TCP ,由于 TCP 是面向流的,一串数据包就像一条河流一样,没有明确的边界,因此可能会出现所谓的“粘包” 现象,而导致解析错误。在 TCP 编程时务必要记住:从一端的输出流调用一次 write()
方法写入数据,到另一端的输入流调用一次 read()
方法读取数据,二者没有必然的对应关系。 如果接收端的缓冲区足够大,调用一次 read()
可能可以读取发送方多次 write()
写入的数据,反之,如果缓冲区不够大,那么就只能 read()
到对方一次 write()
的部分数据。要解决这个问题,可以强制立即发送数据,也可以在发送前进行“封包” ,再在接收的时候进行“拆包” 。
TCP 服务器是一种迭代服务器 ,会按顺序处理客户端请求,采用多线程可以实现并行服务器。