网络编程有两个主要问题:如何准确定位网络上一台或多台主机,以及找到主机后如何可靠、高效地传输数据。在 TCP/IP 里,IP 层负责主机定位和路由,IP 地址唯一确定 Internet 上的一台主机。
TCP 层则提供面向应用的可靠(TCP)或非可靠(UDP)传输,这才是网络编程的主要对象,一般不必关心 IP 如何处理数据。流行模型是客户机/服务器(C/S):服务器作为守护进程始终运行、监听端口;有客户请求就启动一个服务进程响应,同时自己继续听,让后来的客户也能及时得到服务。
一、两类传输协议:TCP 与 UDP
TCP(Transfer Control Protocol)是面向连接、保证可靠传输的协议。得到的是顺序、无差错的数据流。成对的两个 socket 必须先建立连接,然后双向收发。
UDP(User Datagram Protocol)是无连接协议。每个数据报都是独立信息,含完整源/目的地址,在网上走任意可能路径,因此能否到达、何时到达、内容是否正确都不能保证。
| 对比 |
UDP |
TCP |
| 连接 |
数据报自带完整地址,不必先连接 |
传输前必须建立连接,有握手时间 |
| 大小 |
每个数据报须在 64KB 之内 |
连接建立后可按统一格式传大数据 |
| 可靠性 |
不可靠,到达次序也不保证 |
确保接收方正确拿到全部数据 |
| 典型应用 |
视频会议、游戏:要连贯不必绝对正确 |
Telnet、FTP:不定长数据必须可靠 |
可靠传输要付出代价:校验占用 CPU 和带宽。UDP 操作简单、监护少,适合局域网高可靠分散系统里的 C/S。
二、什么是 Socket
网络上两个程序通过双向通讯连接交换数据,链路的一端称为一个 Socket。Socket 通常用来实现客户方和服务方的连接,由一个 IP 和一个端口号唯一确定。Socket 支持的协议不只 TCP/IP 一种,两者没有必然联系。Java 环境下的 Socket 编程主要指基于 TCP/IP 的网络编程。
过程:Server 监听某端口是否有连接请求,Client 发出 Connect,Server 回 Accept,连接建立。双方可用 Send、Write 等与对方通信。完整 Socket 工作四步:
-
-
-
-
关闭 Socket(简单演示程序有时省略显式 close)
三、创建 Socket
java.net 提供 Socket(客户端)和 ServerSocket(服务端):
Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int port);
Socket(String host, int port, boolean stream);
Socket(SocketImpl impl);
Socket(String host, int port, InetAddress localAddr, int localPort);
Socket(InetAddress address, int port, InetAddress localAddr, int localPort);
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr);
address、host、port 是对方 IP、主机名、端口;stream 指明流 socket 还是数据报 socket;localPort 是本地端口;localAddr / bindAddr 是本机地址;impl 是 socket 父类。backlog 表示服务端所能支持的最大等待连接数。例如:
Socket client = new Socket("127.0.0.1", 80);
ServerSocket server = new ServerSocket(80);
选端口要小心。0~1023 为系统保留:HTTP 80,FTP 21,Telnet 23(原文把 ftp/telnet 端口写反了)。自定义服务应选大于 1023 的端口。创建失败会抛 IOException,必须捕获或声明抛出。
四、简单 Client / Server
客户端向 127.0.0.1:4700 发请求,从标准输入读一行,不是 bye 就发给 Server,再打印 Server 回的一行。原文客户端把输入流变量写成 is,后面误用;下面统一为 is。
import java.io.*;
import java.net.*;
public class TalkClient {
public static void main(String args[]) {
try {
Socket socket = new Socket("127.0.0.1", 4700);
// 向本机的 4700 端口发出客户请求
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
PrintWriter os = new PrintWriter(socket.getOutputStream());
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readline = sin.readLine();
while (!readline.equals("bye")) {
os.println(readline);
os.flush();
System.out.println("Client:" + readline);
System.out.println("Server:" + is.readLine());
readline = sin.readLine();
}
os.close();
is.close();
socket.close();
} catch (Exception e) {
System.out.println("Error" + e);
}
}
}
服务器在 4700 监听,accept 后进入同样的对话循环:
import java.io.*;
import java.net.*;
public class TalkServer {
public static void main(String args[]) {
try {
ServerSocket server = null;
try {
server = new ServerSocket(4700);
} catch (Exception e) {
System.out.println("can not listen to:" + e);
}
Socket socket = null;
try {
socket = server.accept(); // accept() 阻塞等待客户请求
} catch (Exception e) {
System.out.println("Error." + e);
}
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter os = new PrintWriter(socket.getOutputStream());
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Client:" + is.readLine());
String line = sin.readLine();
while (!line.equals("bye")) {
os.println(line);
os.flush();
System.out.println("Server:" + line);
System.out.println("Client:" + is.readLine());
line = sin.readLine();
}
os.close();
is.close();
socket.close();
server.close();
} catch (Exception e) {
System.out.println("Error:" + e);
}
}
}
五、支持多客户
实际服务器要同时服务多个客户:永久监听,一有请求就启动专门的服务线程,自己马上回到监听。
ServerSocket serverSocket = null;
boolean listening = true;
try {
serverSocket = new ServerSocket(4700);
} catch (IOException e) { }
while (listening) { // 永远循环监听
new ServerThread(serverSocket.accept(), clientnum).start();
clientnum++;
}
serverSocket.close();
public class ServerThread extends Thread {
Socket socket = null;
int clientnum;
public ServerThread(Socket socket, int num) {
this.socket = socket;
clientnum = num + 1;
}
public void run() {
try {
// 在这里实现数据的接收和发送
} catch (Exception e) { }
}
}
六、Datagram 通讯
传输层除 TCP 还有 UDP。HTTP、FTP、SMTP 等用 TCP;需要强实时交互时(网游、视频会议)常用 UDP。数据报像邮件,不保证必达;面向连接的 TCP 像电话。
DatagramSocket 建立传送数据报的连接,DatagramPacket 表示一个数据报。构造时若发生端口冲突会抛 SocketException,必须处理。
DatagramSocket();
DatagramSocket(int port);
DatagramSocket(int port, InetAddress laddr);
DatagramPacket(byte buf[], int length);
DatagramPacket(byte buf[], int length, InetAddress addr, int port);
DatagramPacket(byte[] buf, int offset, int length);
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);
buf 存放数据,length 是长度,addr 和 port 指明目的地址,offset 是位移。接收前用第一种构造生成 Packet,再 receive() 等待(一直等到有数据报):
DatagramPacket packet = new DatagramPacket(buf, 256);
socket.receive(packet);
发送用带目的地址的构造,再 send():
DatagramPacket packet = new DatagramPacket(buf, length, address, port);
socket.send(packet);
构造数据报时要给 InetAddress,可用 getByName() 从主机名得到 IP。
七、组播 MulticastSocket
DatagramSocket 只能发到一个目的地址。MulticastSocket 允许以广播方式发到该端口的所有客户,用在客户端监听服务器广播。
客户方 MulticastClient.java:加入组 230.0.0.1,循环 5 次接收。
import java.io.*;
import java.net.*;
public class MulticastClient {
public static void main(String args[]) throws IOException {
MulticastSocket socket = new MulticastSocket(4446);
InetAddress address = InetAddress.getByName("230.0.0.1");
socket.joinGroup(address);
DatagramPacket packet;
for (int i = 0; i < 5; i++) {
byte[] buf = new byte[256];
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData());
System.out.println("Quote of the Moment:" + received);
}
socket.leaveGroup(address);
socket.close();
}
}
服务器方启动线程:
public class MulticastServer {
public static void main(String args[]) throws java.io.IOException {
new MulticastServerThread().start();
}
}
MulticastServerThread(原文 Private / FIVE_SECOND / getByte 为笔误,下面按可运行写法):
import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastServerThread extends QuoteServerThread {
private long FIVE_SECONDS = 5000;
public MulticastServerThread(String name) throws IOException {
super("MulticastServerThread");
}
public void run() {
while (moreQuotes) {
try {
byte[] buf = new byte[256];
String dString = null;
if (in == null) dString = new Date().toString();
else dString = getNextQuote();
buf = dString.getBytes();
InetAddress group = InetAddress.getByName("230.0.0.1");
DatagramPacket packet = new DatagramPacket(buf, buf.length, group, 4446);
socket.send(packet);
try {
sleep((long)(Math.random() * FIVE_SECONDS));
} catch (InterruptedException e) { }
} catch (IOException e) {
e.printStackTrace();
moreQuotes = false;
}
}
socket.close();
}
}
一句话总结:定位靠 IP,传输选 TCP(可靠连接)或 UDP(无连接数据报);Java 里 TCP 用 Socket/ServerSocket 四步走,UDP 用 DatagramPacket,组播再用 MulticastSocket。