目录
[二、UDP协议中的socket api](#二、UDP协议中的socket api)
[四、TCP协议中的socket api](#四、TCP协议中的socket api)
一、TCP与UDP的特点
TCP的特点:
- 有连接
- 可靠传输
- 面向字节流
- 全双工
UDP的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 全双工
(一)有连接VS无连接
这是抽象的概念,指的是虚拟的/逻辑上的连接。
- 对于TCP来说,TCP协议中,就保存了对端的信息: A和B通信,A和B先建立连接,让A保存B的信息,B保存A的信息(彼此之间知道要连接的是哪个)。
- 对于UDP来说,UDP协议本身,不保存对方的信息,就是无连接。
(二)可靠传输VS不可靠传输
在网络上,数据是非常容易出现丢失的情况的(丢包),光信号/电信号都可能受到外界的干扰。
在进行通信时,不能指望一个数据包100%地到达对方。
- 可靠传输指的是,虽然不能保证数据包100%到达,但是能尽可能提高传输成功的概率。
- 不可靠传输只是把数据发了,就不管了。
(三)面向字节流VS面向数据报
- 面向字节流指的是在读写数据时,以字节为单位。
- 面向数据报指的是读写数据时,以数据报为单位。
(四)全双工VS半双工
- 全双工指的是 一个通信链路中,支持双向通信(能读也能写)。
- 半双工指的是 一个通信链路中,只支持单向通信(要么读,要么写)。
二、UDP协议中的socket api
计算机中的"文件",是一个广义的概念,文件还能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件,统一管理的)。
UDP协议是用来操作网卡的,将网卡抽象成socket文件,操作网卡的时候,流程和操作普通文件差不多。
(一)DatagramSocket类
DatagramSocket类是用来操作socket文件,发送和接收数据报的。
构造方法:
|--------------------------|------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到主机的任意一个随机端口号(一般用于客户端)。 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到主机的一个指定的端口号(一般用于服务端)。 |
成员方法:
|--------------------------------|----------------------------------|
| 方法签名 | 方法说明 |
| void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接受到数据报,该方法会阻塞等待)。 |
| void send(DatagramPacket p) | 从此套接字发送数据报(不会阻塞等待,直接发送)。 |
| void close() | 关闭此数据报套接字。 |
(二)DatagramPacket类
DatagramPacket就是UDP发送和接收的数据报。
构造方法:
|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| DatagramPacket(byte[]buf,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据报保存在字节数组中(第一个参数buf),接收的指定长度(第二个参数length)。 |
| DatagramPacket(byte[]buf,int offset,int length,SocketAddress address) | 构造一个DatagramPacket用来接收数据报,接收的数据报保存在字节数组中(第一个参数buf),指定起点(第二个参数offset),接收的指定长度(第三个参数length)。address指定目的主机的IP和端口号。 |
成员方法:
|--------------------------|-----------------------------------------------|
| 方法签名 | 方法说明 |
| InetAddress getAddress() | 从接收的数据报中,获取发送端的主机IP地址;或从发送的数据报中,获取接收端的主机IP地址。 |
| int getPort() | 从接收的数据报中,获取发送端的主机的端口号;或从发送的数据报中,获取接收端的主机的端口号。 |
| byte[] getData() | 获取数据报中的数据。 |
(三)InetSocketAddress类
构造UDP发送的数据报时,需要传入SocketAddress(父类),该对象可以使用InetSocketAddress(子类)来创建。
InetSocketAddress的构造方法:
|----------------------------------------------|-------------------------|
| 方法签名 | 方法说明 |
| InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号 |
三、UDP协议的回显服务器
Java数据报套接字通信模型:

(一)UdpEchoServer回显服务器
java
package NetWork;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP协议的回显服务器
//服务器端
public class UdpEchoServer {
private DatagramSocket socket=null;
//指定了一个固定端口号, 让服务器来使用.
public UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//循环一次,就相当于处理一次请求。
//1.读取请求并解析
//创建请求数据报
DatagramPacket RequestPacket=new DatagramPacket(new byte[4096],4096);
//开始接收,并更新数据报
socket.receive(RequestPacket);
//2.根据请求, 计算响应. (服务器最关键的逻辑)
//把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
String request=new String(RequestPacket.getData(),0, RequestPacket.getLength());
String response=process(request);
//3.把响应返回给客户端
//根据 response 构造 DatagramPacket, 发送给客户端.
//此处不能使用 response.length(),因为这是String的长度而不是byte数组的长度
DatagramPacket ResponsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,RequestPacket.getSocketAddress());
//发送构建好的数据报
socket.send(ResponsePacket);
//4.打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", RequestPacket.getAddress().toString(), RequestPacket.getPort(), request, response);
}
}
//服务器根据请求,处理业务
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
(二)UdpEchoClient客户端
java
package NetWork;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//UDP协议的回显服务器
//客户端
public class UdpEchoClient {
DatagramSocket socket=null;
// 客户端要给服务器发送数据报,首先得知道服务器的IP和端口号
private String ServerIp;//目的IP
private int ServerPort;//目的端口号
// 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
this.ServerIp = serverIp;
this.ServerPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner sc=new Scanner(System.in);
while(true){
// 1.读取用户输入的内容
System.out.println("请输入要发送的内容:");
if(!sc.hasNext()){
break;
}
String request=sc.next();
// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
// 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
DatagramPacket RequestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(ServerIp),ServerPort);
// 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.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
client.start();
}
}
(三)拓展:英译汉服务器
当我们需要实现另外一个简单的服务器时,例如英译汉服务器,只需要继承然后重写process方法就可以了。
java
package NetWork;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
//英译汉服务器
public class UdpDictServer extends UdpEchoServer{
private HashMap<String,String> dict=new HashMap<>();
//要在子类的构造方法中调用父类的构造方法
//构造方法初始化字典
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("apple","苹果");
dict.put("boy","男孩");
dict.put("cat","小猫");
dict.put("dog","小狗");
}
//重写process方法
public String process(String request){
return dict.getOrDefault(request,"没有找到该词汇");
}
public static void main(String[] args) throws IOException {
UdpDictServer DictServer=new UdpDictServer(9090);
DictServer.start();
}
}
四、TCP协议中的socket api
(一)ServerSocket类
ServerSocket是创建TCP服务器端Socket的API。
构造方法:
|------------------------|-----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务器端套接字Socket,并绑定到指定端口。 |
成员方法:
|-----------------|-------------------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务器端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待。 |
| void close() | 关闭此套接字 |
(二)Socket类
Socket类是客户端socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务器端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
构造方法:
|------------------------------|----------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host,int port) | 创建一个客户端套接字Socket,并对应IP的主机上对应端口的进程进行连接。 |
成员方法:
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输入流 |
| OutputStream getOutPutStream() | 返回此套接字的输出流 |
五、TCP协议的回显服务器

(一)TcpEchoServer回显服务器
java
package NetWork;
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
private ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
// 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.
ExecutorService executorService = Executors.newCachedThreadPool();
while (true) {
// tcp 来说, 需要先处理客户端发来的连接.
// 通过读写 clientSocket, 和客户端进行通信.
// 如果没有客户端发起连接, 此时 accept 就会阻塞.
// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.
Socket clientSocket = serverSocket.accept();
// 使用多线程的方式来调整
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();
// 使用线程池来调整
executorService.submit(() -> {
processConnection(clientSocket);
});
}
}
private void processConnection(Socket clientSocket){//对clientSocket进行读写操作
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()){
// 针对 InputStream 套了一层
Scanner scanner = new Scanner(inputStream);
// 针对 OutputStream 套了一层
PrintWriter writer = new PrintWriter(outputStream);
while(true){//因为输入流中的数据是持续读取的,要加上循环
// 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.
if (!scanner.hasNext()) {
//scanner.hasNext():判断输入流中是否还有 "下一个令牌"(默认以空白字符分割,如空格、换行等)。
// 连接断开了
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 2. 根据请求计算响应
String request=scanner.next();
String response=process(request);
// 3. 返回响应到客户端
// outputStream.write(response.getBytes());
writer.println(response);
//将缓存区中的数据都发送出去,避免残留
writer.flush();
// 打印日志
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
//服务器连接一个客户端就要创建一个clientSocket,使用完就要关闭.
clientSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
(二)TcpEchoClient客户端
java
package NetWork;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 直接把字符串的 IP 地址, 设置进来.
// 127.0.0.1 这种字符串
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()){
//给inPutStream套一层
Scanner scannerNet= new Scanner(inputStream);
//给outPutStream套一层
PrintWriter writer=new PrintWriter(outputStream);
while (true){
//1.读取用户输入
String request=scanner.next();
//2.发送请求并刷新缓存区数据
writer.println(request);
writer.flush();
//3.接收服务器的响应
String response=scannerNet.next();
//4.打印出响应
System.out.println(response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
注意点:
- 在服务器中,采用多线程的方式来处理客户端的请求(使用线程池)。因为如果是单线程有多个客户端连接,当程序处理processConnection请求时,就可能阻塞在processConnection,而不能accpet。
- 因为服务器中有scanner.hasNext来判断发来的请求,所以客户端发送的请求要以换行符/空白符号结束,因此发送时用writer.println。
- 发送请求后记得使用flush刷新缓冲区的数据。