socket,即套接字,是一种介于应用和传输层之间的抽象层,可实现同网络内不同应用之间相互发送和接收数据。
对于 socket,有以下三个关键问题:
在发送和接收数据时,关键在于如何定位到应用,而定位到应用首先需要定位到主机。在同一子网内,每台主机的 IP 地址是唯一的,故可以借助 IP 地址定位主机,而在每一台主机上,不同的程序往往监听在不同的端口号上,因此,可以利用端口号来定位应用。
因此,一个 socket 由一个 url 地址和一个 port 端口号唯一确定。
socket 与 TCP/IP 协议簇无关,其本质是编程接口,用于向应用提供数据传输服务的接口。Java 中的 socket 主要是基于 TCP/IP 的封装,在使用过程中,
应用可借助socket接口,建立基于TCP或UDP的数据传输机制,从而实现同网络跨应用之间的数据传输功能,从而将应用与传输层的具体协议分离开来,使得上层应用无需关注过多细节,只专注数据传输即可。
服务端代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
public class TCPEchoServer {
private static final int BUFSIZE = 32;
public static void main(String[] args) throws IOException {
// 参数校验,传入参数 端口号
if (args.length != 1) {
throw new IllegalArgumentException("Parameter(s): " );
}
int servPort = Integer.parseInt(args[0]);
// 创建 ServerSocket 实例,并监听给定端口号 servPort
ServerSocket servSock = new ServerSocket(servPort);
int recvMsgSize;
byte[] receiveBuf = new byte[BUFSIZE];
while (true) {
// 用于获取下一个客户端连接,根据连接创建 Socket 实例
Socket clntSock = servSock.accept();
// 获取客户端地址和端口号
SocketAddress clientAddress = clntSock.getRemoteSocketAddress();
System.out.println("Handling client at " + clientAddress);
// 获取 socket 的输入输出流
InputStream in = clntSock.getInputStream();
OutputStream out = clntSock.getOutputStream();
// 每次从输入流中读取数据并写到输出流中,直至输入流为空
while ((recvMsgSize = in.read(receiveBuf)) != -1) {
out.write(receiveBuf, 0, recvMsgSize);
}
// 关闭 Socket
clntSock.close();
}
}
}
必要说明:
客户端代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
public class TCPEchoClient {
public static void main(String[] args) throws IOException {
// 参数校验,传入格式 url "info" 或 url "info" 10240
if ((args.length < 2) || (args.length > 3)) {
throw new IllegalArgumentException("Parameter(s): []" );
}
// 获取目标应用 url
String server = args[0];
// 将传送数据转化为字节数组
byte[] data = args[1].getBytes();
// 解析端口号,若无则设为 10240
int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 10240;
// 根据参数创建 Socket 实例
Socket socket = new Socket(server, servPort);
System.out.println("Connected to server... sending echo string");
// 获取 socket 的输入输出流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// 将数据写入到 Socket 的输出流中,并发送数据
out.write(data);
int totalBytesRcvd = 0;
int bytesRcvd;
// 接收返回信息
while (totalBytesRcvd < data.length) {
if ((bytesRcvd = in.read(data, totalBytesRcvd, data.length - totalBytesRcvd)) == -1) {
throw new SocketException("Connection closed permaturely");
}
totalBytesRcvd += bytesRcvd;
}
System.out.println("Received: " + new String(data));
// 关闭 Socket
socket.close();
}
}
必要说明:
发送数据时只通过 write() 方法,接收时为何需要多个 read() 方法?
TCP 协议无法确定在 read() 和 write() 方法中所发送信息的界限,而且发送过程中可能存在乱序现象,即分割成多个部分,所以无法通过一次 read() 获取到全部数据信息。
服务端代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPEchoServer {
private static final int ECHOMAX = 255;
public static void main(String[] args) throws IOException {
// 参数校验,格式 port
if (args.length != 1) {
throw new IllegalArgumentException("Parameter(s): " );
}
// 获取端口号
int servPort = Integer.parseInt(args[0]);
// 创建数据报文 Socket
DatagramSocket socket = new DatagramSocket(servPort);
// 创建数据报文
DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
while (true) {
// 接收请求报文
socket.receive(packet);
System.out.println("Handling client at " + packet.getAddress().getHostAddress() +
" on port " + packet.getPort());
// 发送数据报文
socket.send(packet);
// 重置缓存区大小
packet.setLength(ECHOMAX);
}
}
}
必要说明:
客户端代码:
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.*;
public class UDPEchoClient {
private static final int TIMEOUT = 3000;
private static final int MAXTRIES = 5;
public static void main(String[] args) throws IOException {
// 参数解析,格式 url "info" 或 url "info" 10240
if ((args.length < 2) || (args.length > 3)) {
throw new IllegalArgumentException("Parameter(s): []" );
}
// 创建目标 Server IP 地址对象
InetAddress serverAddress = InetAddress.getByName(args[0]);
// 将需传输字符转换为字节数组
byte[] byteToSend = args[1].getBytes();
// 获取服务端端口号,默认 10241
int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 10241;
// 创建 UDP 套接字,选择本地可用的地址和可用端口号
DatagramSocket socket = new DatagramSocket();
// 设置超时时间,用于控制 receive() 方法调用的实际最短阻塞时间
socket.setSoTimeout(TIMEOUT);
// 创建发送数据报文
DatagramPacket sendPacket = new DatagramPacket(byteToSend, byteToSend.length, serverAddress, servPort);
// 创建接收数据报文
DatagramPacket receivePacket = new DatagramPacket(new byte[byteToSend.length], byteToSend.length);
// 设置最大重试次数,以减少数据丢失产生的影响
int tries = 0;
// 是否收到响应
boolean receivedResponse = false;
do {
// 将数据报文传输到指定服务器和端口
socket.send(sendPacket);
try {
// 阻塞等待,直到收到一个数据报文或等待超时,超时会抛出异常
socket.receive(receivePacket);
// 校验服务端返回报文的地址和端口号
if (!receivePacket.getAddress().equals(serverAddress)) {
throw new IOException("Received packet from an unknown source");
}
receivedResponse = true;
} catch (InterruptedIOException e) {
tries += 1;
System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
}
} while (!receivedResponse && (tries < MAXTRIES));
if (receivedResponse) {
System.out.println("Received: " + new String(receivePacket.getData()));
} else {
System.out.println("No response -- giving up.");
}
// 关闭 Socket
socket.close();
}
}
必要说明:
由于 UDP 提供的是尽最大可能的交付,所以在发送 Echo Request 请求时,无法保证一定可以送达目标地址和端口,因此考虑设置重传次数,若在超过最大等待时间后仍未收到回复,则重发当前请求,若重发次数超过最大重试次数,则可直接返回未发送成功。
Socket 的设计与实现,可以看做是对 TCP 和 UDP 在应用层的一种封装方式,对于有连接的 TCP,如何控制连接过程,以准确传输数据对于精确度要求高的程序就比较重要。而对于 UDP,如何提供尽可能便捷、尽可能快的方式就比较重要。
在实际应用中,根据具体需求选择合适的形式即可。
[1] Java TCP/IP Socket编程