目录
一、什么是UDP与TCP
二、什么是Socket
三、UDP网络编程简单实现
1、DatagramSocket API
2、DatagramPacket API
3、基本使用方法:
我们以一个简易的翻译器的案例,来实现简单的UDP编程:
服务器端代码:
部分代码的说明:
客户端代码:
部分代码说明:
测试结果:
四、TCP网络编程简单实现
ServerSocket API
Socket API
接下来以一个简单的回显服务器来说明TCP编程。
1、建立服务器端
部分代码说明:
2.建立客户端
测试结果:
TCP与UDP是计算机网络中五层模型的运输层协议。
UDP协议:(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可 靠传输。 因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高
TCP 协议:是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
对比来看两个协议的特点:
TCP:有连接;可靠传输;面向字节流;全双工
UDP:无连接;不可靠传输;面向数据报;全双工
有连接:比如打电话,得先接通,才能相互交互数据
无连接:想发微信,不需要接通,直接就能发数据
可靠传输:传输过程中,发送方知道接受方有没有收到数据
不可靠传输:传输过程中,发送方不知道接收方有没有收到数据
面向字节流:依字节为单位进行传输(非常类似于文件操作中的字节流)
面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接受必须是一个完整的数据报,不能是半个或者一个半
全双工:一条链路,双向通信(双行道)
半双工:一条链路,单向通信(单行道)
在我们进行网络编程时,都会接触到一个名叫Scoket的概念,应用程序通过Scoket来建立远程连接,Socket通过内部封装好的协议把数据传输到网络。网络编程套接字,是操作系统给应用程序提供的一组API(叫做socket API)
为什么需要Socket 进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果 只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket 接口,每个应用程序需 要各自对应到不同的Socket,数据包才能根据 Socket正确地发到对应的应用程序。socket可以视为应用层和传输层之间的通信桥梁。
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收,因此,当Socket 连接成功地在服务器端和客户端之间建立后:对服务器端来说,它的Socket是指定的IP地址和指定的端口号。
UDP和TCP相比,就简单的一点,因为UDP不需要建立连接,也就没有区分哪一个是客户端哪一个是服务器端,是依靠数据包来进行实现的,数据包也是收一个发一个,不存在使用流的概念。
UDP也是需要使用Socket来监听端口的的,不过它使用的是java提供的DatagramSocket。
DatagramSocket API是UDP Socket,用于发送和接受UDP数据报
DatagramPacket
是UDP Socket发送和接收的数据报
注:构造UDP发送的数据时,需要传入SocketAddress,该对象可以使用InetSocketAddress来创建。
服务端:
客户端:
我们以一个简易的翻译器的案例,来实现简单的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 UdpEchoServer {
//创一个DatagramSocket对象中
private DatagramSocket socket = null;
//翻译就是从key——>value的过程
private Map dict = new HashMap<>();
//构造方法:
//参数的端口表示我们的服务器要绑定的端口
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
//这里利用HashMap存放大量的key,value值
dict.put("玫瑰","rose");
dict.put("许愿","Wishing");
dict.put("不期而遇",
"unexpected encounters ");
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//UDP不需要建立连接,直接接收从客户端传来的数据
//死循环:不断接收客户端的连接
while(true) {
//每循环一次,处理一次请求
//1、读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//接收
socket.receive(requestPacket);
//把这个datagramPacket对象转成字符串,方便打印
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2、根据请求计算响应
String response = process(request);
//3、把响应写回到客户端
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 Data) {
return dict.getOrDefault(Data,"词典中未找到");
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
在构造方法中,port参数的是什么意思:
port参数,就是表示该服务器要绑定的端口,目的在于能够让客户端明确是访问主机上的哪个进程,通过端口确定一个进程,就需要在这个进程启动的时候绑定一个端口,并且一个端口只能被一个进程绑定(一般)
为什么使用死循环:
服务器是不知道客户端啥时候发送请求,所以需要时刻准备着,接收
receive() 方法说明:
这个方法的参数,是个输出型参数,调用receive的时候,就需要构造一个空的 DatagramPacket对象,然后把对象交给receive,在receive里面负责把从网卡读到的数据,给填充到这个对象里面
注:构造对象时,里面的new byte[4096],这个是自己调整大小,不要太小就好了
两次构造对象的区别:
requestPacket.getSocketAddress()的作用:
记录客户端的IP和端口号
例如:
我买了一个快递,现在想要退货,就需要把这个包裹发回给商家,商家的收货地址和收件人都是在我收到的包裹上写着的。
日志打印:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//构造方法:
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
//自动让系统指定一个空闲的窗口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
//客户端
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true) {
//1、从控制台读取用户输入的内容
System.out.println("->");
String request = sc.next();
//2、构造一个UDP请求,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIP),this.serverPort);
//发送
socket.send(requestPacket);
//3、从服务器读取UDP响应数据,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
//接收
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4、把服务器的响应显示到控制台上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
构造方法的参数:
public UdpEchoClient(String serverIP,int serverPort) {}
前者是服务器IP,后者是服务器端口
疑问?为什么服务器那边,只是端口号,因为服务器一般情况下,IP就是本机IP
为什么让系统自动指定窗口?
socket = new DatagramSocket();
一般情况下,都是系统自动指定的,如果是手动指定,刚好所指定的窗口正在被别人使用,就会引来不必要的麻烦
举例:
我们去餐厅吃饭,每次去我们都喜欢坐在一个拐角处(指定窗口),突然有一天,这里被别人坐了,我们总不能去赶走人家吧,最好还是做个空位做下就好啦,所以通常都是有系统自动指定一个空闲的窗口
关于DatagramPacket构造:
整体的运行流程:
测试结果:
TCP的编程实现客户端与服务器端交互,是通过建立连接,从而利用“流”来进行数据交换的,即使用字节输入/输出流,把要发送的数据,存储到字节数组,通过“流”来实现发送与接收。
接下来以一个简单的回显服务器来说明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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
//代码中会涉及到多个socket对象,使用不同的名字来区分
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool();
while(true) {
//1、先调用accept来接受客户端的连接
//如果当前没有客户端来建立连接,accept就会阻塞
Socket clientSocket = listenSocket.accept();
//2、再处理这个连接,这里应该要使用多线程,每个客户端上来都分配一个新的线程负责处理
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort() );
//接下来处理客户端的请求
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while(true) {
//1、读取请求并解析
Scanner sc = new Scanner(inputStream);
if(!sc.hasNext()) {
//读完了,连接断开了
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString());
break;
}
String request = sc.next();
//2、根据请求计算响应
String response = process(request);
//3、把响应写回给客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//刷新缓冲区确保数据确实是通过网卡发送出去了
printWriter.flush();
System.out.printf("%s:%d req:%s;resp:%s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}catch(IOException e) {
e.printStackTrace();
} finally {
//这个关闭socket
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
为什么使用线程池?
为了实现一个服务器能够并发响应多个客户端的请求,这里引入多线程的方法:
1,因为listen()监听函数过后,服务器的ip与端口就会暴露在网络中,网络中连接的各个客户端就可以连接该服务器,而所有的连接请求都会存储在监听文件描述符对应的读缓冲区中,每执行一次accept,就会从该监听文件描述符对应的读缓冲区中读取一个连接,因此,如果是多线程服务器,应该在主线程中将accept函数包含在一个while(true)循环中,让主线程不断从该缓冲区中接收连接。
2,当accept函数执行完以后,就要有对应的子线程处理accept函数返回的客户端,因此,在while循环内部,每当执行完accept成功以后,就创建一个子线程,让该线程去处理该客户端。子线程内部的流程就是与客户端互相交流的一些代码。
为什么关闭socket
socket也是一个文件,一个进程能够同时打开的文件个数有上限(PCB文件描述符表是有限的),listenSocket对象在TCP服务器程序中,只有一个唯一的对象,一般不会把文件描述符表占满(随着进程的结束,自动释放)。而clientSocket是在死循环里面的,每次来一个客户端,建立连接,都要分配一个,这个对象就会被反复创建按销毁实例,每创建一个,都要销毁一个文件描述符,因此需要把不再使用的clientSocket及时释放掉
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 {
//客户端需要使用这个socket对象来建立连接
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
//和服务器建立连接,就需要知道服务器在哪儿了
//这里和UDP客户端差别比较大
socket = new Socket(serverIP,serverPort);
}
public void start() {
Scanner sc = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
//1、从控制台读取数据,构成一个请求
System.out.println("->");
String request = sc.next();
//2、发送请求给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
//这个flush不要忘记,否则可能导致请求没有真发出去
printWriter.flush();
//3、从服务器读取响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4、把响应显示到界面上
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
测试结果:
下期见啦!!!