目录
网络编程的目的:通过网络,让不同主机之间能够进行通信。
在进行网络编程的时候,需要操作系统提供一组API,也就是Socket API,才能完成编程。Socket API可以认为是应用层和传输层之间交互的路径。
Socket:也被称为套接字,就是网络中不同主机上的应用程序之间进行双向通信的端点的抽象。其本质上是一种特殊的文件。与普通文件不同的是,普通文件都是在同一台计算机上,两个进程之间传输数据。而Socket可以实现在不同计算机之间传输数据,也就是网络传输数据。比如说打开QQ,打开一个网页,这些都是socket来实现通信的,而网络通信又必须遵守的两个协议,tcp/ip协议和udp协议,Socket里面已经封装好了这两个协议,我们直接拿来用即可。Socket就属于是把"网卡"这个设备给抽象成文件了。往Socket文件中写数据,就相当于通过网卡发送数据;从Socket文件读数据,就相当于通过网卡接收数据。
传输层提供的网络协议主要有两个:TCP和UDP。
详细来说这四个区别。
1. TCP是有连接的,UDP是无连接的。
2. TCP是可靠传输的,UDP是不可靠传输的。
举个例子:A给B发送消息,发送成功还是失败,若A这边都能及时知道,那这就是可靠传输,否则就是不可靠传输。但是要想可靠传输,也得付出一些代价:
3. TCP是面向字节流的,UDP是面向数据报的。
4. TCP和UDP都是全双工的。
一个信道,允许双向通信,就是全双工。
一个信道,只能单向通信,就是半双工。
双向通信的原因:与网线有关,一根网线里通常有8根网线,4个为一组,每一组又是单向传输的,这样这一组负责数据传过来,另一组负责数据传过去。就构成了双向通信。
在Java中也提供了一组API,用来操作Socket,具体使用DatagramSocket这个类操作。其常用方法有接收数据receive()方法,发送数据send()方法,关闭文件close()方法。DatagramPacket这个类用来创建数据包,每次发送数据或者是接收数据都要以数据包的形式传递。且发送数据时数据包还要加上发送目的地(IP,端口等)。接收数据时还要提前创建好空的数据报。
为啥要以数据包的形式接收和发送?
因为UDP是面向数据包的。是以数据报为基本单位。
接下来我们实现一个简单的基于UDP的客户端/服务器通信的程序。(回显服务器)
服务器程序
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
-
- //服务器程序
- public class MyServer {
- private DatagramSocket socket = null;
- //port 端口号 服务器需要手动指定端口号
- public MyServer(int port) throws SocketException {
- //折磨写就是手动指定端口
- socket = new DatagramSocket(port);
- //这么写就是让系统自动分配端口
- //socket = new DatagramSocket();
- }
- //服务器启动方法start
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while(true) {
- // 1.读取请求并解析
- //创建一个空的数据报,用来存储服务器接收客户端传来的请求
- DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
- //讲数据报传入socket中
- socket.receive(requestPacket);
- //将数据报中存储的客户端的请求数据从字节流转成字符串
- String request = new String(requestPacket.getData(),0,requestPacket.getLength());
- // 2.根据请求计算响应
- String response = process(request);
- //将计算后的响应数据一个传入新创建的数据报 参数有 字节数组 字节数组长度 数据报的发送地址
- DatagramPacket responsePacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
- requestPacket.getSocketAddress());
- // 3.把响应写回给客户端 以数据包形式
- socket.send(responsePacket);
- // 4.打印日志
- System.out.printf("[%s:%d] request=%s response=%s\n",requestPacket.getAddress().toString(),
- responsePacket.getPort(),request,response);
- }
- }
-
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- //设置服务器的端口号为9090
- MyServer server = new MyServer(9090);
- server.start();
- }
-
- }
客户端程序
- 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 MyClient {
- private DatagramSocket socket = null;
- //服务器IP
- private String serverIp =null;
- //服务器端口
- private int serverPort = 0;
-
- //构造方法 参数为服务器IP和服务器端口号
- public MyClient(String IP,int port) throws SocketException {
- //客户端端口号不需要手动指定 由系统自动分配
- socket = new DatagramSocket();
- this.serverIp = IP;
- this.serverPort = port;
- }
- //客户端启动程序
- public void start() throws IOException {
- System.out.println("客户端启动!");
- Scanner sc = new Scanner(System.in);
- while(true) {
- // 1.将客户端的请求数据发给服务器
- System.out.print("->");
- String request = sc.next();
- //创建数据报 将请求数据以字节流形式装入包中
- //因为是充当发送的数据报 所以数据包中要有 数据+发送目的地
- DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
- //将数据报发送给服务器
- socket.send(requestPacket);
- // 2.接收服务器响应回的数据
- //再创建一个空的数据包接收服务器返回的数据
- DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
- //接收服务器返回的响应
- socket.receive(responsePacket);
- String response = new String(responsePacket.getData(),0,responsePacket.getLength());
- System.out.println(response);
- }
- }
- public static void main(String[] args) throws IOException {
- //参数为服务器IP地址和端口号
- MyClient client = new MyClient("127.0.0.1",9090);
- client.start();
- }
- }
基于UCP协议的话:服务器和客户端之间的来回receive和send都是以数据包形式传递数据 所以要经常创建数据包。数据包中可包含数据 发送的目的地。充当发送的数据包中是要有发送的地址的,充当接收的数据包要预先创建一个空的数据包。这就是两种方式下数据包的不同的用法。
上述代码中的几个细节:
1.既然说Socket是文件,为啥使用完后不进行close操作呢?不怕文件资源泄露吗?
文件资源泄露的原因:在一个程序中,频繁地打开文件而不去关闭。会使文件描述符表满。
2.服务器和客户端的端口号都需要手动添加吗?
3.这段代码服务器和客户端各自的工作流程?
4.为啥接收数据的时候需要提前手动创建一个有大小的数据报?
基于上述代码再写一个简单的翻译器程序(继承)
-
- import java.io.IOException;
- import java.net.SocketException;
- import java.util.HashMap;
- import java.util.Map;
-
- public class udpTranServer extends udpServer{
- private Map
map = new HashMap<>(); - public udpTranServer(int port) throws SocketException {
- super(port);
- map.put("cat","猫");
- map.put("dog","狗");
- map.put("tiger","老虎");
- //......
- }
-
- //重写process方法 服务器处理请求
- @Override
- public String process(String request) {
- //英译汉
- String response = map.getOrDefault(request,"没有这个单词!");
- return response;
- }
-
- public static void main(String[] args) throws IOException {
- udpTranServer server = new udpTranServer(9090);
- server.start();
- }
- }
有两个关键的类:
ServerSocket:只给服务器使用,用来绑定端口号。
Socket:既给服务器用,也给客户端用,用来接收(receive)和发送(send)数据(字节流)。
常用的四个方法:

由于TCP是面向字节流的,以字节为基本单位。所以传输过程中就会用到InputStream和OutputStream流进行接收和发送。通过InputStream进行read操作,就是接收,通过OutputStream进行write操作,就是发送。
服务器
- 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 {
-
- private ServerSocket serverSocket = null;
- public tcpServer(int port) throws IOException {
- //服务器手动指定端口号
- serverSocket = new ServerSocket(port);
- }
-
- //这个方法用来启动服务器
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while(true) {
- //服务器接收请求
- Socket requestSocket = serverSocket.accept();
- //先需要建立连接 细节流程是由内核完成的 只需要调用即可
- processConnection(requestSocket);
- }
- }
-
- private void processConnection(Socket requestSocket) throws IOException {
- //打印日志
- System.out.printf("[%s:%d] 上线啦! %s\n",requestSocket.getInetAddress(),requestSocket.getPort());
- //处理请求
- try(InputStream is = requestSocket.getInputStream();
- OutputStream os = requestSocket.getOutputStream()) {
- Scanner sc = new Scanner(is);
- PrintWriter writer = new PrintWriter(os);
- while(true) {
- // 字节流转字符流
- // 先读 Scanner
- if (!sc.hasNext()) {
- System.out.printf("[%s:%d] 下线啦!\n",requestSocket.getInetAddress(),requestSocket.getPort());
-
- break;
- }
- String request = sc.next();
- // 处理请求
- String response = process(request);
- //打印日志
- System.out.printf("[%s:%d] req=%s res=%s\n",requestSocket.getInetAddress(),requestSocket.getPort(),request,response);
-
- // 将请求处理结果返回给客户端
- // 再写 PrintWriter 或者将 字符串转成字节数组
- writer.println(response);
- // 字符串转成字节数组
- // os.write(response.getBytes());
- // 刷新缓冲
- writer.flush();
- // os.flush();
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- requestSocket.close();
- }
-
- }
-
- private String process(String request) {
- //这里负责处理请求并返回响应
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- tcpServer server = new tcpServer(9090);
- server.start();
- }
- }
客户端
- 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 {
- Socket socket = null;
- //需要明确访问服务器的IP地址和端口号
- public tcpClient(String serverIp,int serverPort) throws IOException {
- //具体链接的细节,不需要我们手动干预,内核自动负责
- //当我们new这个对象的时候,操作系统就进行三次握手 具体细节,完成建立的细节
- socket = new Socket(serverIp,serverPort);
- }
- public void start() {
- System.out.println("客户端启动!");
- Scanner sc = new Scanner(System.in);
- try(InputStream is = socket.getInputStream();
- OutputStream os = socket.getOutputStream()) {
- Scanner res = new Scanner(is);
- PrintWriter writer = new PrintWriter(os);
- while(true) {
- System.out.print("->");
- String request = sc.next();
- //将请求以字节流形式发送给服务器
- //os.write(request.getBytes());
- writer.println(request);
- //刷新缓冲区
- writer.flush();
- //os.flush();
- //接收服务器传回来的响应
- //传回来的是字节流 进一步要转成字符流
- String response = res.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",9090);
- client.start();
- }
- }
问题一:为啥光Socket对象要进行close操作而ServerSocket不用close,为啥这个会资源泄露?
原因是这个对象在while循坏中,每次有一个客户端来请求都会创建一个,这个Socket对象也就会占据文件描述符的位置,久而久之,不及时关闭就会造成文件资源泄露,而同一个代码中ServerSocket不用close,是因为它的对象只有一个,且生命周期贯穿整个程序。
但是我们不是加了try()语句吗,这种语句形式不是会自动关闭的吗?
原因是这只是关闭了Socket对象自带的流对象,本身本体Socket对象并没有被关闭。
问题二:上面这种代码如果启动多个客户端会有什么问题,如何解决?
虽然客户端是多个,但其实进线程只有一个。 最先启动的客户端是可以正常交互的。而后面启动的这些客户端没有响应。并且当第一个客户端结束后,第二个又可以正常运行,并且会将之前积压的请求由服务器全部正常处理。以此类推,即同一时间只能正常运行一个,仿佛是其他客户端都在等待自己的前一个客户端结束后才活过来。这是与我们服务器的代码逻辑有关,在代码中,我们有两层嵌套while(true)循环,这样第一个客户端上线后,进程就会阻塞在里面的while循环的判断语句hasNext这块,还是上面所说的虽然客户端是多个,但其实线程只有一个。所以只有这个用户下线后,里面的while循环才能跳出来,继续执行。
解决办法:使用多线程并发执行。
1. 把连接操作放在Thread里
2. 采用线程池
线程池的方式虽然可以降低线程的频繁创建与销毁的开销。但如果同一时刻大量用户进行访问,线程池也要同时创建多个线程,也会招架不住。还可以采用协程(3)或IO多路复用(4)。
3.协程
4.IO多路复用/IO多路转接
IO多路复用/IO多路转接:用一个线程可以同时处理多个客户端的Socket。在Java中是NIO,其底层就是IO多路复用。
代码如下:
只需要改服务器程序。
- 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 tcpServer {
-
- private ServerSocket serverSocket = null;
- //使用线程池 保证多个客户端正常运行
- ExecutorService service = Executors.newCachedThreadPool();
- public tcpServer(int port) throws IOException {
- //服务器手动指定端口号
- serverSocket = new ServerSocket(port);
- }
-
- //这个方法用来启动服务器
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while(true) {
- //服务器接收请求
- Socket requestSocket = serverSocket.accept();
- //先需要建立连接 细节流程是由内核完成的 只需要调用即可
-
- //第一种 最简单的多线程处理
- /*
- Thread thread = new Thread(() -> {
- try {
- processConnection(requestSocket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- });
- thread.start();
- */
- //第二种 线程池
- service.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(requestSocket);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- });
- //协程和IO多路复用以后实现
- }
- }
-
- private void processConnection(Socket requestSocket) throws IOException {
- //打印日志
- System.out.printf("[%s:%d] 上线啦! \n",requestSocket.getInetAddress(),requestSocket.getPort());
- //处理请求
- try(InputStream is = requestSocket.getInputStream();
- OutputStream os = requestSocket.getOutputStream()) {
- Scanner sc = new Scanner(is);
- PrintWriter writer = new PrintWriter(os);
- while(true) {
- // 字节流转字符流
- // 先读 Scanner
- if (!sc.hasNext()) {
- System.out.printf("[%s:%d] 下线啦!\n",requestSocket.getInetAddress(),requestSocket.getPort());
-
- break;
- }
- String request = sc.nextLine();
- // 处理请求
- String response = process(request);
- //打印日志
- System.out.printf("[%s:%d] req=%s res=%s\n",requestSocket.getInetAddress(),requestSocket.getPort(),request,response);
-
- // 将请求处理结果返回给客户端
- // 再写 PrintWriter 或者将 字符串转成字节数组
- writer.println(response);
- // 字符串转成字节数组
- // os.write(response.getBytes());
- // 刷新缓冲
- writer.flush();
- // os.flush();
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- }finally {
- requestSocket.close();
- }
-
- }
-
- private String process(String request) {
- //这里负责处理请求并返回响应
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- tcpServer server = new tcpServer(9191);
- server.start();
- }
- }