在我们初识完网络知识后,我们了解了关于TCP/IP五层模型
分别是:应用层,传输层,网络层,数据链路层,物理层
这篇文章,我们主要了解关于传输层中相关的俩种协议:UDP TCP
1.UDP和TCP的特点
2.UDP网络编程
3.TCP网络编程
在传输层中,传输层为应用层提供了编程接口(即一组类或者方法)给我们使用 ---- socket
提供了俩组socket,分别是UDP和TCP
UDP和TCP的特点
传输层涉及到了俩种最主要的协议,UDP协议和TCP协议
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
有连接 / 无连接
连接就是让俩台主机能够互相的进行通信,互相保存了对方的核心信息(IP和端口号)
断开连接就是删除对方的核心信息
TCP在正式通信之前,就需要保存好双方的关键信息,通信完毕之后再进行删除
UDP在正式通信之前,就不需要
可靠传输 / 不可靠传输
我们在进行网络通信的过程,往往有时候会出现丢包的现象(正常现象)
TCP是可靠传输,他就是在对抗丢包现象的出现,尽可能的将数据包传输给对方,但是有时候还是会出现丢包的现象,无法达到100%的传输,但也可以知道对方是否接收到了数据包
UDP则不考虑是否丢不丢包的情况,爱咋咋地吧~~~~这样传输效率就会比较高,而TCP传输效率就会比较低,毕竟他需要要关注丢包的情况
面向字节流 / 面向数据报
面向字节流:与读文件类似,读写的方式非常的灵活,可以按照规定的字节,分多次进行读取
面向数据报:每次读和写,要以一个UDP数据报为单位进行
全双工 / 半双工
全双工:一个通信,可以进行双向传输
半双工:一个通信,只能单向传输
UDP网络编程


其中DatagramPacket就是UDP数据传输的基本单位
1.)实现一个回显的UDP客户端和服务器
当客户端发送一串字符串,服务器应该原封不动的返回,然后输出在显示器上 ---- 回显
服务器端Code:
java
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
//服务端
public class UdpServer {
private DatagramSocket socket = null;//先不初始化
//构造方法 --- port就是端口号
public UdpServer(int port) throws SocketException {
socket = new DatagramSocket(port) ;
}
//启动服务器
public void start() throws IOException {
System.out.println("Server Start!!!!");
//因为服务器是7*24h不断工作,等待客户端发来的请求~~~所以使用死循环
while(true) {
//1.接收请求并解析
//接收请求,需要先申请一个DatagramPacket内存空间
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//DatagramPacket是以数据包为单位进行读取~~~
//receive的构造参数时DatagramPacket,所以需要传给他Datagrampacket
socket.receive(requestPacket);//客户端请求接收
//因为客户端发送的请求中是二进制数组,我们需要将DatagramPacket转换为字符串 ---- 获取二进制数据,并从下标0到length转换为字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应(因为我们只需要在显示器打印东西,所以不需要计算)
//根据请求计算相应,这里我们实现的是回显操作,客户端发送什么,服务器就返回什么即可~~~~
String response = process(request);
//3.将响应重新返回给客户端
//需要DatagramPacket,传字节数组,长度,getSocketAddress(会返回ip和port),相应也是按照DatagramPocket数据包为单位进行发送~~~~
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//send构造参数也是DatagramPacket,所以需要我们将字符串转换为DatagramPacket
socket.send(responsePacket);
//4.打印(打印ip,端口号,请求内容,相应内容)
System.out.printf("[%s : %d] request : %s, resopnse : %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return request;//因为这里是回显操作,客户端发送什么,服务器返回什么即可,不需要计算的操作
}
public static void main(String[] args) throws IOException {
UdpServer server = new UdpServer(10086);
server.start();
}
}
客户端Code:
java
package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//客户端
public class UdpClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public UdpClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("client start!!!");
Scanner scanner = new Scanner(System.in);
while(true) {
//1.让用户从控制台输入字符串
System.out.print("请输入一串字符串:");
String request = scanner.next();
if(request.equals("exit")) {
break;
}
//2.将用户输入的字符串转换为UDP数据包类型,因为send的类型是数据包类型~~~
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.serverIp), this.serverPort);
socket.send(requestPacket);//将请求发送给服务器
//3.获取服务器的响应 --- receive构造也是DatagramPacket,所以也需要构造一个DatagramPacket,注意这里获取的是字节数组
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
//接收服务器响应
socket.receive(responsePacket);
//转换为字符数组,因为服务器发送的响应是一个二进制字节数组,我们需要转换为String,然后打印的显示器上
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
//4.显示响应
System.out.println("响应数据为:" + response);
}
}
public static void main(String[] args) throws IOException {
UdpClient client = new UdpClient("127.0.0.1", 10086);//告诉客户端服务器的ip和port
client.start();
}
}


这里有几点要注意:
1.我们让用户输入的是字符串,但是DatagramPacket参数是二进制,所以需要将字符串转换为二进制
2.服务器返回的是二进制,客户端需要接收并且转换为字符串形式,才能显示在显示器上,否则就是乱码
3.服务器应该设置为While死循环,因为服务器是7*24小时服务的
4.在编写服务器的时候,需要我们程序员指定的给定一个端口号,而客户端是不需要给定端口号的,是一个无参数的构造方法,操作系统会自动分配一个空闲的端口号
5.服务器需要接收客户端的请求并且解析 ---> 根据请求计算响应 ----> 将响应返回给客户端
6.客户端需要从控制台读取用户输入的字符串 ---> 构造请求发送给服务器 ---> 读取服务器返回的响应 ---> 显示响应结果到显示器上
2.)实现一个查字典的UDP客户端和服务器
客户端输入一个中文单词,服务器要求返回这个中文对应的英文单词,然后客户端接收,并且显示到显示器上
这里就会使用Map保存好中文和对应的英文单词~~~
服务器Code:
java
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//翻译服务器
public class UdpDictionaryServer {
private DatagramSocket socket = null;
//创建词典
public Map<String, String> map = new HashMap<>();
public UdpDictionaryServer (int port) throws SocketException {
socket = new DatagramSocket(port);
map.put("苹果", "apple");
map.put("香蕉", "banana");
map.put("猪", "pig");
map.put("鸭子", "duck");
map.put("老虎", "tiger");
map.put("鱼", "fish");
map.put("你好", "hello");
}
public void start() throws IOException {
System.out.println("Server Start~~~");
while(true) {
//1.接收请求并解析 --- 需要使用receive接收,receive的构造方法是DatagramPakcet
DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(requestPacket);
//receive接收到的是一个二进制字节数组,需要转换为字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
//2.根据请求计算响应
String response = process(request);
//3.计算好的响应返回给客户端 -- 需要使用send,但是注意,send构造也是需要DatagramPacket的~~~,这里参数是获取字节数组,字节数组长度,以及接收到请求的IP和Port
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.显示打印
System.out.printf("[%s : %d] request : %s, resopnse : %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
}
}
public String process(String request) {
return map.getOrDefault(request, "很抱歉没查找到此单词~~~");
}
public static void main(String[] args) throws IOException {
UdpDictionaryServer server = new UdpDictionaryServer(10086);
server.start();
}
}
客户端Code:
java
package UDP;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//翻译客户端
public class UdpDictionaryClient {
DatagramSocket socket = null;
private String ip;
private int port;
public UdpDictionaryClient(String ip, int port) throws SocketException {
socket = new DatagramSocket();
this.ip = ip;
this.port = port;
}
public void start() throws IOException {
System.out.println("Client Start~~~");
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.print("请输入中文:");
String dictionary = scanner.next();
if(dictionary.equals("exit")) {
break;
}
//1.发送请求给服务器 --- 需要使用到send,构造方法是DatagramPacket
DatagramPacket request = new DatagramPacket(dictionary.getBytes(), dictionary.getBytes().length, InetAddress.getByName(this.ip), this.port);
socket.send(request);
//2.接收服务器的响应 --- 需要使用到receive,构造方法是DatagramPacket
DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
socket.receive(responsePacket);
//因为服务器响应回来的是一个二进制字节数组,我们需要转换为字符串
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println("翻译后结果为:" + response);
}
}
public static void main(String[] args) throws IOException {
UdpDictionaryClient client = new UdpDictionaryClient("127.0.0.1",10086);
client.start();
}
}


TCP网络编程
TCP提供了俩个类:ServerSocket(给服务器使用) 和 Socket(即给客户端,也给服务器使用)
ServerSocket的任务是:负责将连接到服务器的客户端进行统一处理
Socket的任务是:针对每一个客户端,提供具体的服务(读写数据)
1.)实现回显的TCP客户端和服务器
服务器Code:
java
package TCP;
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 TcpServer {
//创建ServerSocket对象
private ServerSocket serverSocket = null;
public TcpServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("Server Start~~~");
while(true) {
//需要接收客户端的连接 --- 需要使用到accept,如果接收到了,会返回,否则就一直阻塞等待
Socket socket = serverSocket.accept();//这里的socket会保存客户端的ip和port,每一次连接客户端,就会创建一个socket连接,但是很快就执行完,所以执行完后需要关闭连接~~
//处理连接过程,但是服务器只有一个,当我们启动多个客户端,一个服务器无法接收多个客户端的请求,这时就需要创建多线程,让多线程去实现多个服务器,才能接收不同客户端的请求
Thread t = new Thread(() -> {
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
public void processConnection(Socket socket) throws IOException {
//一次连接的过程中,客户端和服务器可能会传输多组数据
System.out.printf("【%s : %d】 客户端上线\n", socket.getInetAddress(), socket.getPort());
//因为TCP使用的是字节流,所以我们可以使用InputStram---读,OutputStream---写
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter write = new PrintWriter(outputStream);
while(true) {
//1.处理多次请求,这里一次循环就处理一个请求
if(!scanner.hasNext()) {
System.out.printf("【%s : %d】 客户端下线\n",socket.getInetAddress(), socket.getPort());
break;
}
String request = scanner.next();
//2.根据请求计算响应 --- 这里实现的是回显操作,直接返回客户端请求即可
String response = process(request);
//3.将响应返回给客户端
write.println(response);
//刷新缓冲区
write.flush();
//4.打印日志
System.out.printf("【%s : %d】 request ->%s response ->%s\n", socket.getInetAddress(), socket.getPort(), request, response);
}
}catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();//当客户端和服务器连接完毕,并执行完上面代码,这个连接就失效了,我们需要手动关闭socket的连接,防止资源泄露!!!!
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpServer server = new TcpServer(10086);
server.start();
}
}
每次服务器收到一个客户端的连接,都是通过ServerSocket的进行accept
ServerSocket只需要进行accept即可,不负责提供读写的操作
客户端Code:
java
package TCP;
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 TcpClient {
private Socket socket = null;
public TcpClient(String ip, int port) throws IOException {
socket = new Socket(ip, port);
}
public void start() {
System.out.println("Client Start~~~");
try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
PrintWriter write = new PrintWriter(outputStream);
Scanner read = new Scanner(inputStream);
while(true) {
//1.让用户输入字符串
System.out.print("请输入一串字符串 ->");
String request = scanner.next();
//2.将请求发送给服务器
write.println(request);
//刷新缓冲区
write.flush();
//3.接收服务器响应
if(!read.hasNext()) {
break;
}
String response = read.next();
//将响应输出在屏幕上
System.out.println(response);
}
}catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpClient client = new TcpClient("127.0.0.1", 10086);
client.start();
}
}
对于TCP的网络编程,有三点需要注意:
1.)TCP是面向字节流的,即InputStream / OutputStream,会对文件进行操作,所以文件内部是设置了缓冲区,我们写入的文件,并不是直接写入文件,而是写入到了一个缓冲区的地方,当缓冲区满了,才会真正写入到文件中 ---- 所以在write的时候,需要使用flush对缓冲区进行刷新,即使缓冲区没有满,也是可以真正写入的
2.)当前代码中是存在文件资源泄露的情况,因为当客户端每次连接上服务器,都会触发一次accept,都会创建一个Socket,就会占用一个文件资源描述符表的位置!!!当客户端源源不断的过来,每一次的连接都会产生一个新的socket文件,而我们只使用,不关闭,就会一直占用文件描述符表的位置 ---- 这时就需要我们进行close操作
3.)我们需要使用多线程,才能让多个客户端同时进行操作,如果随着客户端数量不断增加,我们后续还可以继续考虑线程池,减小线程的创建与销毁的操作
2.)实现翻译字典的TCP客户端和服务器
服务器Code:
java
package TCP;
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.HashMap;
import java.util.Map;
import java.util.Scanner;
public class TcpDictionaryServer {
private ServerSocket serverSocket = null;
private Map<String, String> map = new HashMap<>();
public TcpDictionaryServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
map.put("你好", "hello");
map.put("鸭子", "duck");
map.put("鱼", "fish");
map.put("猪", "pig");
map.put("狗", "dog");
map.put("猫", "cat");
}
public void start() throws IOException {
System.out.println("Server Start~~~");
//与客户端产生连接,连接成功会返回
Socket socket = serverSocket.accept();
Thread t = new Thread(() -> {
//使用多线程,让多个客户端可以运行,避免一个服务器只能服务一个客户端
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
t.start();
}
public void processConnection(Socket socket) throws IOException {
//客户端和服务器一次连接,之间可能传输较多数据
System.out.printf("【%s : %d】 客户端上线\n", socket.getInetAddress(), socket.getPort());
try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter write = new PrintWriter(outputStream);
while(true) {
if(!scanner.hasNext()) {
System.out.printf("【%s : %d】 客户端下线\n",socket.getInetAddress(), socket.getPort());
break;
}
//接收请求
String request = scanner.next();
//计算响应
String response = process(request);
//将请求返回给客户端
write.println(response);
//刷新
write.flush();
//打印日志
System.out.printf("【%s : %d】 request ->%s response ->%s\n", socket.getInetAddress(), socket.getPort(), request, response);
}
}catch (IOException e) {
e.printStackTrace();
}finally {
socket.close();//当客户端与服务器连接,执行完上面的代码,连接就失效了,这时需要释放socket,防止资源泄露!!!!
}
}
public String process(String request) {
return map.getOrDefault(request, "很抱歉,没查找到此单词~~~");
}
public static void main(String[] args) throws IOException {
TcpDictionaryServer server = new TcpDictionaryServer(10086);
server.start();
}
}
客户端Code:
java
package TCP;
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 TcpDictionaryClient {
private Socket socket = null;
public TcpDictionaryClient(String ip, int port) throws IOException {
socket = new Socket(ip, port);
}
public void start() {
System.out.println("Client Start~~~");
try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(System.in);
PrintWriter write = new PrintWriter(outputStream);
Scanner read = new Scanner(inputStream);
while(true) {
//让用户输入内容
System.out.print("请输入一个中文:");
String request = scanner.next();
//发送请求
write.println(request);
write.flush();//刷新缓冲区
//接收服务器响应
if(!read.hasNext()) {
break;
}
String response = read.next();
//将服务器响应输入到显示器上
System.out.println("英文是:" + response);
}
}catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpDictionaryClient client = new TcpDictionaryClient("127.0.0.1", 10086);
client.start();
}
}

