一、UDP
UDP:无连接,不可靠传输,面向数据报,全双工。
1.无连接:UDP 客户端没有建立连接,先是 new 了一个 DatagramSocket 对象,然后就直接开始发送请求了。
2.不可靠传输:代码层次看不出来,在内核中才体现出来。
3.面向数据报:不管是服务器还是客户端,发送和接收都是以数据报(DatagramPacket)为基本单位进行的。
4.全双工:一个 scoket 既可以发送,又可以接收。
1.UdpEchoServer 代码(服务器)
public class UdpEchoServer {
// 要想创建 UDP 服务器,首先要打开一个 socket 文件(构造方法中实例化)
private DatagramSocket socket = null;
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[4096], 4096);
// 1.读取客户端发来的请求
socket.receive(requestPacket);
// 2.对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 3.根据请求,计算响应
String response = process(request);
// 4.把响应构造成 DatagramPacket 对象
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length, requestPacket.getSocketAddress());
// 5.把这个 DatagramPacket 对象返回给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] request = %s; response = %s\n", requestPacket.getAddress(),
requestPacket.getPort(), request, response);
}
}
// 通过这个方法,实现根据请求计算响应这个过程
// 如果是其他服务器,就可以在 process 里面,加上一些其他的逻辑处理
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
// 真正启动服务器
UdpEchoServer server = new UdpEchoServer(8000);
server.start();
}
}
2.强化process,升级为“自定义翻译”服务器
public class UdpDictServer extends UdpEchoServer{
private Map<String,String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 构造词汇
dict.put("hello","你好");
dict.put("monkey","孙悟空");
dict.put("fuck","卧槽");
dict.put("dog sun","狗日");
}
// 重写 process
@Override
public String process(String request) {
return dict.getOrDefault(request,"这个问题俺也不会!");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(8000);
server.start();
}
}
3.UdpEchoClient 代码(客户端)
public class UdpEchoClient {
private DatagramSocket socket = null;
public UdpEchoClient() throws SocketException {
// 客户端的端口号,一般都是由操作系统自由分配的。
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1.让客户端从控制台读取一个请求数据
System.out.println("> ");
String request = scanner.next();
// 2.把这个字符串请求发送给服务器,构造 DatagramPacket
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName("127.0.0.1"), 8000);
// 3.把数据报发送给服务器
socket.send(requestPacket);
// 4.从服务器读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 5.把响应的数据获取出来,转成字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.printf("request: %s; response: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient();
client.start();
}
}
二、TCP
1.TcpEchoServer代码(服务器)
public class TcpEchoServer {
public ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 此处最好使用自动扩容版本的线程池
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
// 如果当前没有客户端来建立连接,就会阻塞等待!
Socket clientSocket = serverSocket.accept();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnect(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 通过这个方法,给当前连上的这个客户端,提供服务
// 一个连接过来了,服务方式可能有两种:
// 1.一个连接只进行一次数据交互(一个请求 + 一个响应),也叫做短链接
// 2.一个连接进行多次数据交互(N 个请求 + N 个响应),也叫做长链接
public void processConnect(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 建立连接!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
// 这里是长连接的写法,需要通过循环来获取多次交互
while(true) {
if(!scanner.hasNext()) {
// 断开连接
System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 1.读取请求并解析
String request = scanner.next();
// 2.根据请求计算响应
String response = process(request);
// 3.把响应写回给客户端
printWriter.println(response);
// 刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] request: %s; response: %s\n",
clientSocket.getInetAddress().toString(), clientSocket.getPort(),request,response);
}
} finally {
// 建立连接后,就没用了,需要关闭资源,避免资源泄露
clientSocket.close();
}
}
// 回显
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(8000);
tcpEchoServer.start();
}
}
2.TcpEchoClient代码(客户端)
public class TcpEchoClient {
public Socket socket = null;
public TcpEchoClient() throws IOException {
// new 这个对象,需要和服务器建立连接,建立连接,就需要知道服务器在哪
socket = new Socket("127.0.0.1",8000);
}
public void start() throws IOException {
// 由于实现的是长连接,一个连接会处理 N 个请求和响应
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 包装一下输入输出流
Scanner scannerNet = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while(true) {
// 从控制台读取用户的输入
System.out.println("> ");
String request = scanner.next();
// 2.向服务器发送请求
printWriter.println(request);
printWriter.flush();
// 3.从服务器读取响应
String response = scannerNet.next();
// 4.显示服务器响应
System.out.printf("request: %s; response: %s\n",request,response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient();
tcpEchoClient.start();
}
}
三、需要注意的点
在使用原生字节流的时候,记得包装成 Scanner , PrintWriter 形式(或者字符流)。
此处还要注意的地方就是,我们将输出流包装成 PrintWriter ,如果使用其中的 writer 方法进行发送数据,上面的代码就会有问题,因为客户端要从控制台读取用户的输入,输入完成后的回车被 Scanner(System.in) 给读取走了,那么服务这边一定会在读取请求并解析这里阻塞(String request = scanner.next() 阻塞),就会导致程序卡死。所以保险起见,我们这里在使用 PrintWriter 的时候,使用它的 println 方法,自动换行,就不会幺蛾子了。