目录
一. 什么是socket套接字
二. socket套接字
2.1 socket套接字根据传输层协议分类
2.2 TCP流套接字 UDP数据报套接字主要特点
三. UDP数据报套接字编程
3.1 DatagramSocket 是UDP socket, 用于发送和接受数据报
3.2 DatagramPacket 是UDP socket 发送和接收的数据报
3.3 练习案例
回显服务器
回显客户端
四. TCP字节流套接字编程
4.1 ServerSocket
4.2 Socket
4.3 练习案例
回显服务器
回显客户端
注意:
一. 什么是socket套接字
socket套接字是由系统提供用于网络通信的技术, 是基于TCP/IP协议的网络通信的基本操作单元.
基于Socket套接字的网络程序开发就是网络编程.
简单来说, socket就是网络编程套接字, 用来对网卡进行操作.
读网卡就是从网上上收数据, 写网卡就是让网卡发数据.
二. socket套接字
2.1 socket套接字根据传输层协议分类
socket套接字根据传输层协议分类:
1. 字节流套接字 使用传输层TCP协议 Transmission Control Protocol(传输控制协议)
2. 数据报套接字 使用传输层UDP协议 User Datagram Protocol(⽤⼾数据报协议)
2.2 TCP流套接字 UDP数据报套接字主要特点
TCP流套接字: 有连接, 可靠传输, 面向字节流, 全双工
UDP数据报套接字: 无连接, 不可靠传输, 面向数据报, 全双工
1. 有连接 vs 无连接
TCP, 保存通信双方信息 (例如: A与B通信, A与B建立连接)
UDP, 不保存通信双方信息, 但是可以自己实现代码来保存双端信息.
2. 可靠传输 vs 不可靠传输
TCP, 尽可能提高传输成功的概率, 如果出现丢包, 可以感知到.
UDP, 只要把数据发送了, 就不管了.
3. 面向字节流 vs 面向数据报
TCP, 以字节为单位, 读写数据.
UDP, 以数据报为单位, 读写数据.
4. 全双工 vs 半双工
一个通信链路中, 支持双向通信(能读也能写) => TCP/UDP
~, 支持单向通信(要么读, 要么写)
三. UDP数据报套接字编程
3.1 DatagramSocket 是UDP socket, 用于发送和接受数据报
构造方法
成员方法
3.2 DatagramPacket 是UDP socket 发送和接收的数据报
构造方法
成员方法
3.3 练习案例
回显服务器
package UDPNet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class UDPEchoServer {
DatagramSocket socket = null;
public UDPEchoServer(int serverPort) throws IOException {
socket = new DatagramSocket(serverPort); // 指定服务器端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
executorService.submit(() -> {
while (true) { // 客户端发送多次请求
// 1. 接收请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket); // 输出型参数
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 计算响应
String response = process(request);
// 3. 返回响应
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
// 打印日志
System.out.printf("[%s:%d] req:%s res:%s\n", requestPacket.getAddress(), requestPacket.getPort(), request, response);
}
});
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException{
UDPEchoServer udpEchoServer = new UDPEchoServer(3306);
udpEchoServer.start();
}
}
回显客户端
package UDPNet;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UDPEchoClient {
DatagramSocket socket = null;
String serverIP;
int serverPort;
UDPEchoClient(String serverIP, int serverPort) throws IOException {
this.serverIP = serverIP;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 接收用户输入
String request = scanner.nextLine();
// 2. 将请求发送到客户端
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length,
InetAddress.getByName(serverIP), serverPort);
socket.send(requestPacket);
// 3. 接收服务器响应并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println("服务器的响应为: " + response);
}
}
public static void main(String[] args) throws IOException {
UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 3306);
udpEchoClient.start();
}
}
四. TCP字节流套接字编程
4.1 ServerSocket
用于创建TCP服务端socket.
构造方法
成员方法
4.2 Socket
用于创建TCP客户端socket, 或者是服务端与客户端建立连接(accept)后返回的服务端socket.
构造方法
成员方法
4.3 练习案例
回显服务器
package TCPNet;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TCPEchoServer {
// ServerSocket 服务器使用
// socket 服务器和客户端都使用
private ServerSocket serverSocket = new ServerSocket();
public TCPEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port); // 给服务器分配端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService executors = Executors.newCachedThreadPool();
while (true) { // 服务器可能与多个客户端建立连接
Socket socket = serverSocket.accept(); // 服务器与客户端建立连接, 返回的是服务端socket
// socket.getInetAddress 返回的是与服务器连接的客户端的IP地址.
// socket.getPort 返回的是与服务器建立连接的客户端的Port端口号
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress(), socket.getPort());
executors.submit(() -> {
try {
processConnection(socket); // 处理一次连接
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
private void processConnection(Socket socket) throws IOException {
try (socket; InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writerNet = new PrintWriter(outputStream);
while (true) { // 一次连接中可能会有多次请求
if (!scannerNet.hasNextLine()) { // 从网卡中读不到数据了
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
break;
}
// 1. 接收请求并解析
String request = scannerNet.nextLine();
// 2. 计算响应
String response = process(request);
// 3. 返回响应
writerNet.println(response); // 将response放在内存缓冲区中, 并没有让网卡发送数据到客户端
// 为什么要放在内存缓冲区中呢?
// 因为TCP是面向字节流的, 数据是零散的, 放在缓冲区中等数据达到一定数量再发送.
// 平衡了IO速度, 减少了发送次数, 提高了程序效率
writerNet.flush(); // 刷新缓冲区, 让网卡发送数据到客户端
// 打印日志
System.out.printf("[%s:%d] req:%s res:%s\n", socket.getInetAddress(), socket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 一个客户端断开连接, 关闭客户端
// 没有使用finally, 而是使用try with
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException{
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}
回显客户端
package TCPNet;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TCPEchoClient {
Socket socket = null;
public TCPEchoClient(String serverIP, int serverPort) throws IOException {
socket = new Socket(serverIP, serverPort);
}
public void start() throws IOException{
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writerNet = new PrintWriter(outputStream);
while (true) { // 一次连接中多次请求
// 1. 用户输入请求
String request = scanner.nextLine();
// 2. 将请求发送到服务器
writerNet.println(request);
writerNet.flush();
// 3. 接收响应
String response = scannerNet.nextLine();
// 打印响应
System.out.println("服务器的响应为: " + response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException{
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
注意:
(1) 问题
如果只是单线程, 则服务器无法连接多个客户端, 这是因为服务端中的scannerNet.hasNextLine()阻塞, 导致不能进入外层循环, 服务器无法与其他客户端建立连接(accept).
(2) 解决
引入多线程和线程池, 把processConnection操作作为一个任务提交到线程池中. 就意味着一个客户端的连接相当于一个任务, 任务之间互不冲突.