文章目录
一、UDP和TCP
Socket API 是操作系统给应用程序提供的来进行网络数据的 发送和接收的api(即传输层给应用层使用的api)。
在需要通过操作系统来执行的传输层里,提供了两个最核心的协议:UDP和TCP。因此Socket API也提供了两种风格:UDP、TCP。下面我们来看看UDP和TCP两种方式有什么区别。
TCP:有连接,可靠传输,面向字节流,全双工。
UDP:无连接,不可靠传输,面向数据报,全双工。
二、Udp版本客户端服务器
1、DatagramSocket和DatagramPacket(数据报)
DatagramSocket类的相关方法:
构造方法:
- 进程关联了端口号,本质上是进程里的Socket对象关联了 端口号。同时一个进程可以创建多个Socket对象,每个Socket对象都可以连接到不同的网络地址和端口。因此一个进程可以关联多个端口,但一个端口只能关联一个进程。
普通方法:
- receive方法中的DatagramPacket是我们创建的传入的一个空的对象,当receive接收到发送方发来的数据报时,才把发送方发来的内容填充进入我们传入的这个空的DatagramPacket对象,得到接收到的数据报,这个参数也叫做"输出型参数"。
DatagramPacket类(数据报)的相关方法:
构造方法:
普通方法:
2、UdpEchoSever&&UdpEchoClient
2.1、什么是Echo Sever?
2.2、UDP客户端+UDP回显服务器代码
客户端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
// 客户端的ip是环回ip(127.0.0.1),端口是操作系统随机分配的一个端口
// 因为在本机模拟通信,所以服务器的ip也是环回ip(127.0.0.1),端口是程序员指定的
// 服务器的ip和端口都得告诉客户端,我们才能在客户端访问服务器
private String serverIp = null;
private int serverPort = 0;
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
this.socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
// 1.从控制台读取数据到一个空的DatagramPacket中
System.out.print("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("客户端关闭!");
break;
}
// 注意1:InetAddress.getByName(serverIp)操作把点分十进制的ip(127.0.0.1)转换成32位二进制数
// 注意2:发送数据报时,使用String的getBytes().length方法获取数据报长度
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length
,InetAddress.getByName(serverIp), this.serverPort);
// 2.把DatagramPacket发给服务器
socket.send(requestPacket);
// 3.使用空的DatagramPacket,接收服务器处理后的响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket); // 注意:如果receive没有接收到响应数据,那就会阻塞等待。
// 4.打印响应结果
// 注意1:打印返回的响应结果,不能用toString,因为你无法为DatagramPacket类重写toString方法
// 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度
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();
}
}
服务器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
// 注意:1.这个socket对象在操作系统内核中操作时,是当成文件的方式操作,把这个对象当成网卡的抽象
private DatagramSocket socket = null;
// 注意:2.服务器端需要手动指定一个端口,避免客户端找不到服务器
public UdpEchoServer(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
// 1.给一个空的DatagramPacket,用于接收客户端发来的数据报
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket); // 注意:如果receive没有接收到请求数据,那就会阻塞等待。
// 注意1:为了便于处理,把DatagramPacket这个特殊的对象转化成字符串的形式,但是不能用toString,因为你无法为DatagramPacket类重写toString方法
// 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
// 2.对请求内容进行业务处理(这里是回显服务器直接返回)
String response = process(request);
// 3.构造好响应的DatagramPacket,并把它发回客户端。
// (注意1:这里也可以直接使用requestPacket.getSocketAddress()同时获取IP和端口,客户端的端口和ip是requestPacket自带的。
// 注意2:第二个参数必须是字节数组长度response.getBytes().length,而不是字符串的长度
// 使用String的getBytes().length方法获取数据报长度)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
,requestPacket.getAddress(),requestPacket.getPort());
socket.send(responsePacket);
// 4.为了观察,打印一下客户端发来的的信息
System.out.printf("[%s,%d] req:%s; resp:%s\n",requestPacket.getAddress(),requestPacket.getPort()
,request, response);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer sever = new UdpEchoServer(9090);
sever.start();
}
}
执行顺序:
1.服务器先启动,进行到receive进行阻塞,等待客户端发送请求数据报(服务器)
2.客户端读取用户输入内容到请求数据报(客户端)
3.客户端执行send把请求数据报发给服务器(客户端)
4.客户端发送请求数据报后立即执行到receive,等待服务器发来响应数据报(客户端)
服务器接收到请求数据报,从服务器的receive阻塞中返回(服务器)
5.服务器根据请求数据报计算响应数据报(服务器)
6.服务器执行send,发送响应数据报给客户端(服务器)
7.客户端从receive阻塞中返回,读到响应数据报(客户端)
2.3、查词典服务器代码
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictSever extends UdpEchoServer{
// 使用一个集合来存放单词集合
private Map<String,String> dict = new HashMap<>();
public UdpDictSever(int port) throws SocketException {
super(port);
dict.put("cat","猫");
dict.put("beautiful","美丽的");
dict.put("perfect","完美的");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"没有你要查的单词!");
}
public static void main(String[] args) throws IOException {
UdpDictSever sever = new UdpDictSever(9090);
sever.start();
}
}
三、Tcp版本客户端服务器
1、ServerSocket和Socket
ServerSocket类的相关方法:
构造方法:
普通方法:
Socket类的相关方法:
构造方法:
普通方法:
2、TcpEchoServer&&TcpEchoClient
2.1、Tcp客户端
Tcp版本的客户端和Udp版本的客户端的区别:
客户端:
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 {
// 注意1:在客户端new一个Socket对象的时候,就连接服务器。
// 注意2:Socket对象可以字节把点分十进制的serverIp转换成32位二进制数
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动!");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
while (true){
// 1.客户端从控制台读取用户输入的内容
System.out.print(">");
String request = scanner.next();
if (request.equals("exit")){
System.out.println("客户端关闭!");
break;
}
// 2.客户端把请求写入网卡,发送给服务器处理
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request); //注意:要写入"\n"
printWriter.flush(); // 冲刷,保证数据写入网卡
// 3.客户端读取服务器响应写回到网卡上的数据
Scanner respScan = new Scanner(inputStream);
String response = respScan.next();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
客户端代码步骤:
1. 客户端从控制台读取用户输入的内容
2. 客户端把请求写入网卡,发送给服务器处理
3. 客户端读取服务器响应写回到网卡上的数据
4. 打印响应的结果
2.2、Tcp服务器
Tcp版本的服务器和Udp版本的服务器的区别:
Tcp版本的服务器需要注意的点:
-
1> Tcp版本的服务器需要在发送消息时在数据后面加上
\n
。因为接收端读取数据时使用Scanner的next方法读取,next方法规则是:读到换行符/空格/tab时结束,读到的数据不包含以上符号。所以发送端可以在数据的结尾加上\n
,表示读取数据结束。这个点客户端也是一样。如下图:printWriter.println(outputStream)
表示在发送数据outputStream后面加上一个\n
。发送outputStream后,一定记得flash,把信息真正的发送。
-
2> 在Tcp版本的服务器端中,需要关闭客户端访问时创建的Socket资源。每次有一个客户端访问服务器,就会创建一个Socket对象和客户端的Socket连接。服务器端每创建一个Socket对象,就在服务器的这个进程上的文件描述符表上占用一个空间,而客户端访问量应该是很多的。因此如果连接完成后,不关闭这个Socket,到了文件描述符表位置被占满时,其它客户端就无法再访问服务器了,因此,在每个客户端连接完成后,我们需要关闭服务器端的这个Socket资源,释放这个Socket占用的文件描述符表的位置。
那么为什么Udp版本的服务器不需要关闭?Udp版本服务器端的的DatagramSocket的生命周期是整个进程。而Tcp版本的clientSocket的生命周期是每个客户端连接时,断开连接,这个Socket就没用了,且因为每创建一个客户端连接,服务器就会创建一个clientSocket,所以数量上也会很多! -
3> 短连接和长连接:下列代码的processConnection中的while去掉就是短连接,即传输一次就断开连接,每次访问都得先连接再发送请求;长连接即用while,当一个客户端连接好服务器然后发送请求后,先不断开连接,等待用户再次发送请求,等用户自己退出时才断开连接。
-
4> IO多路复用,如果客户端访问量很大,即使使用多线程服务器压力还是很大,就需要用IO多路复用。比如C10K问题(1w个客户端),C10M问题(1kw个客户端访问)。IO多路复用,可以使用一个线程处理多个客户端的任务。原理:在这个线程中使用一个集合来存放连接对象,这个线程就负责监听这个集合,在集合中哪个连接有数据来了,线程就处理这个连接。在操作系统中提供了select,epoll就可以监听。
服务器:
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 TcpEchoSever {
private ServerSocket serverSocket = null;
// 注意:服务器本身使用ServerSocket和端口绑定连接
public TcpEchoSever(int Port) throws IOException {
serverSocket = new ServerSocket(Port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
// 注意:使用while保证每次有客户端连接时都能连接到
while (true){
版本一:使用多线程
// // 注意:每当有一个客户端连接服务器时,创建一个Socket对象和客户端的Socket进行通信
// Socket clientSocket = serverSocket.accept();
// // 注意:建立连接使用当前线程,放在我们创建的线程外;使用多线程去处理客户端发来的请求(处理业务)
// Thread t = new Thread(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
// 版本二:使用线程池
Socket clientSocket = serverSocket.accept();
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
// 注意:一个连接对应一个客户端,
private void processConnection(Socket clientSocket) throws IOException {
// 注意:服务器的每一个Socket对应一个客户端
System.out.printf("[%s:%d] 客户端上线!\n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
// 注意:由于一个客户端可能要处理多个请求和响应,所以使用循环进行
while (true){
// 1.服务器读取客户端写入网卡的字节流数据
Scanner reqScan = new Scanner(inputStream);
if (!reqScan.hasNext()){
System.out.printf("[%s:%d] 客户端下线!\n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = reqScan.next();
// 注意:next读到换行符/空格/tab结束,但是读取的内容不包含换行符/空格等
// 我们这里是从客户端的请求内容就读取,所以客户端发来的请求中应当有以上结束符
// 2.对请求进行业务处理
String response = process(request);
// 3.服务器把响应内容写回网卡,响应给客户端
// 操作:用outputStream构造一个PrintWriter字符流对象,便于把"\n"一并写入网卡
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
printWriter.flush(); // 冲刷,保证数据写入网卡
// 4.打印日志
System.out.printf("[%s:%d] req:%s; resp:%s\n",
clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoSever sever = new TcpEchoSever(9090);
sever.start();
}
}
服务器代码步骤:
1. 服务器读取客户端写入网卡的字节流数据
2. 对请求进行业务处理
3. 服务器把响应内容写回网卡,响应给客户端
执行顺序:
1.服务器先启动,进行到accept进行阻塞,等待客户端new Socket从而建立连接(服务器)
2.客户端从控制台读取用户输入内容(客户端)
3.客户端使用OutputStream把请求发给服务器(客户端)
4.服务器Socket感知到请求并使用InputStream接收请求(服务器)
5.服务器根据请求计算响应(服务器)
6.服务器使用OutputStream把响应发回客户端(服务器)
7.客户端Socket感知到请求并使用InputStream接收请求(客户端)
8.客户端打印响应结果
三、UDP和TCP总结