操作系统为我们实现了传输层及以下的协议,程序猿要做的主要是实现应用层方面的协议,也就是网络编程。
目录
网络编程
Socket套接字
UDP数据报套接字
TCP流套接字
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。即使是同一个主机,只要进程不同,通过网络传输数据也属于网络编程的范围。
学习网络编程,首先需要了解:
假如我去餐馆吃饭,那么我就是客户端,餐馆就是服务器。我要了一份炒饭,这就是请求,餐馆把炒饭做出来并放到我旁边,就是响应。
Socket套接字,是基于TCP/IP协议的网络通信的基本操作单元,用于网络通信。基于Socket套接字的开发就是网络编程。
Socket套接字针对传输层协议分为三类:
流套接字:使用传输层TCP协议。
数据报套接字:使用传输层UDP协议。
原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据。
TCP协议特点:
UDP协议特点:
UDP数据报套接字需要用到DatagramSocket的相关API。
DatagramSocket的核心方法是receive()和send(),同时,需要一个DatagramPacket类,也就是数据报来接收请求或者响应,UDP服务器实现流程是这样的:
当然,只要客户端发起请求,服务器都要响应,因此服务器需要循环的处理响应。
public class UdpEchoServer {
private DatagramSocket socket;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//创建接收信息的空间
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
//接收信息
socket.receive(requestPacket);
//处理信息
String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), StandardCharsets.UTF_8);
String response = process(request);//这里的响应由开发者实现
//发送信息到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());//最后一个参数是客户端的地址(请求与响应需要对应)
socket.send(responsePacket);
//打印信息
System.out.printf("[%s: %d] req: %s, res: %s\n",
requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
//这里计算响应结果
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
注意这里的字符串应该先转换为byte数组在计算长度,假如字符串中包含汉字等字符,那么得出的长度就不对了。
与之相对的客户端类似:
客户端不需要直到自己的端口号,因此不必手动指定(客户端的数量很多,如果端口号重复则这些客户端都无法使用),交给系统自动分配即可。但是客户端需要指定服务器的IP和端口号。
客户端流程:输入请求->用数据报接收->send到服务器,使用数据报接收响应结果->使用响应结果
public class UdpClient {
private final DatagramSocket socket;
private final String serverIP;
private final int serverPort;
public UdpClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
serverIP = ip;
serverPort = port;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("-> ");
//输入请求
String request = scanner.next();
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIP), serverPort);//最后两个参数用于指定服务器的IP和端口号
//发送请求到服务器
socket.send(requestPacket);
//接收服务器的处理结果
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
//打印信息
String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), StandardCharsets.UTF_8);
System.out.printf("req: %s, res: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpClient client = new UdpClient("127.0.0.1", 9090);
client.start();
}
}
同样需要注意输入的请求先转换为byte数组再计算长度。
TCP流套接字的实现与UDP大不相同,其实从二者的特点就可以看出来。
TCP流套接字不再使用DatagramSocket类,而是使用ServerSocket和Socket类,其中ServerSocket只做一件事,那就是与客户端建立连接,剩下的交给Socket类处理。
TCP服务器实现流程:
由于客户端与服务器建立了连接,当客户端断开连接时,需要及时的调用close方法关闭连接(虽然不关闭也问题不大,但最好还是关闭,毕竟占用了资源)。
public class TcpEchoServer {
private final ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
//等待客户端连接
System.out.println("等待客户端连接…");
Socket clientSocket = serverSocket.accept();
processConnect(clientSocket);
}
}
private void processConnect(Socket clientSocket) {
System.out.printf("[%s: %d] 客户端连接成功!\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream()) {
try (OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
//循环处理请求
while (true) {
if (!scanner.hasNextLine()) {
//读取完毕
System.out.printf("[%s: %d] 客户端断开连接!\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
String request = scanner.nextLine();
String response = process(request);
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush();//刷新缓冲区,确保第一时间获取请求
System.out.printf("[%s: %d] req: %s, res: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();//关闭资源
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
//这里处理请求返回响应
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
对应的客户端实现流程:创建客户端实例对象(使用Socket类),构造时需要传入参数:IP和端口号(这里的IP和端口号为连接的服务器IP和端口号)->输入请求->请求通过PrintWriter写入outputStream中->刷新缓冲区,把请求提交给服务器->接收服务器返回的响应:
public class TcpClient {
private Socket socket;
public TcpClient(String ServerIP, int serverPort) throws IOException {
socket = new Socket(ServerIP, serverPort);//相当于打电话的拨号
}
public void start() {
try (InputStream inputStream = socket.getInputStream()) {
try (OutputStream outputStream = socket.getOutputStream()) {
System.out.println("连接成功!");
Scanner scanner = new Scanner(System.in);
while (true) {
//输入请求
System.out.print("-> ");
String request = scanner.nextLine();
//发送请求到服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//接收服务器响应结果
Scanner resScanner = new Scanner(inputStream);
String response = resScanner.nextLine();
//输入结果
System.out.printf("req: %s res : %s\n", request, response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpClient client = new TcpClient("127.0.0.1", 9090);
client.start();
}
}