网络编程

网络编程

一.UDP

1.1 api

1.1.1 DatagramSocket

DatagramSocket是一个Scocket对象。那么Socket又是什么呢?

我们的操作系统中,往往使用文件这样的概念来管理一些软硬件资源。我们这里的Socket文件,就是用来管理网卡的一种文件。

Java中的Socket对象,就对应了系统里的Socket文件,要进行网络通信,必须得先有Socket对象。

DatagramSocket()
DatagramSocket(int port)

DatagramSocket有带端口和不带端口的构造方法,那么我们应该如何选择呢?

当在客户端使用时,我们往往让系统来自动分配,而在服务端时,我们往往手动指定。

1.1.2 DatagramPacket

DatagramPacket表示了一个UDP数据报,代表了系统中设定的UDP数据报的二进制结构。

UDP是面向数据报的,每次进行传输,都要以UDP数据报为基本单位。

下面是DatagramPacket的几个构造方法,DatagramPacket作为UDP数据报,必然要承载一部分的数据,这就需要我们通过手动指定的byte[]作为数据存储的空间

public DatagramPacket(byte[] buf, int length,InetAddress address, int port)
public DatagramPacket(byte[] buf, int length)    
    ....

1.2 代码示例

1.2.1echo回显服务器
package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //创建一个DatagramScocket对象
    //这是后续操作网卡的基础
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
        /*
        我们在服务器和客户端中均需要创建一个Socket对象
        但是服务器的socket一般需要显示的指定一个端口号
        而客户端的socket一般不能显示指定(不显示指定时候系统会自动分配一个随机的端口)
         */
    }

    //通过这个方法开启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //我们在使用socket.receive来接受数据时,需要先创建一个数据报(DatagramPacket)用来接受从网卡读取到的数据,
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //1.读取请求并解析
            socket.receive(requestPacket);
            //当前完成recive之后,数据以二进制的形式存储到DatagramPacket中
            //要想能够把数据显示出来,还需要把二进制转成字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            //2.由于我们此处是回显服务器,所以请求=响应。
            String reponse = process(request);
            //3.把响应写回客户端
            //  搞一个响应对象,DatagramPacket
            //  往DatagramPacket中填入刚才的数据,在通过sent返回
            //传入数据报,数据报长度,和请求中的地址
            DatagramPacket reponsePacket = new DatagramPacket(reponse.getBytes(),reponse.getBytes().length,requestPacket.getSocketAddress());
            //注意这个长度不可以更改成reponse.length,因为如果这个字符串的内容都是英文字符,此时字节和字符个数是一样的,但是如果包含中文就不一样了。
            socket.send(reponsePacket);
            //4.打印日志
            System.out.printf("[%s:%d] req=%s res=%s\n",reponsePacket.getAddress().toString(),reponsePacket.getPort(),request,reponse);

        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {

        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}
package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String severIp = "";
    private int severPort = 0;
    public UdpEchoClient(String severIp,int severPort) throws SocketException {
        //创建这个对象不能手动指定端口
        socket = new DatagramSocket();
        //由于UDP自身不回持有对端的信息,就需要在应用程序中把对端的情况记录下来。
        this.severIp = severIp;
        this.severPort = severPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner sc = new Scanner(System.in);
        while(true){
            //1.从控制台读取一个数据,作为请求
            System.out.print("->");
            String request = sc.next();
            //2.把请求内容构造成一个DatagramPacket对象,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName((severIp)), severPort);
            socket.send(requestPacket);
            //3.尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            //4.把响应转换成字符串,并打印输出
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
        udpEchoClient.start();
    }
}

1.2.2 翻译服务器
package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
	private Map<String, String> dict = new HashMap<>();

	public UdpDictServer(int port) throws SocketException {
		super(port);
		dict.put("dog", "小狗");
		dict.put("cat", "小猫");
	}

	@Override
	public String process(String request) {
		return dict.get(request);
	}

	public static void main(String[] args) throws IOException {
		UdpDictServer server = new UdpDictServer(9090);
		server.start();
	}
}

1.2.3 总结

1.服务器先启动,服务器启动之后,就会进入循环,找到receive并阻塞(当客户端还未传入数据时)

2.客户端启动,也会先进入while循环,执行sc.next,并且也在这里阻塞

​ 当用户在控制台输入字符串之后,next就会返回,从而构造请求数据并发送给服务器。

3.客户端发送出数据之后

​ 服务器:就会从receive中返回,进一步的执行解析请求为字符串,执行process操作,执行send操作

​ 客户端:继续往下执行,执行到receive等待服务器的响应、

4.客户端收到从服务器返回的数据后,就会从receive中返回

​ 执行到这里的打印操作,接着显示出响应内容

5.服务器完成一次循环之后,又会执行到reveive

​ 客户端完成一次循环之后,又会执行到sc.next

​ 双双进入阻塞等待

2.TCP

2.1 API

TCP的socketapi和UDP的socket api差异很大,接下来让我们往下看

2.1.1 ServerSocket

给服务器使用的类,用来绑定端口号

2.1.2 Socket

既会给服务器用,又会给客户端使用

2.1.3 对比
  1. TPC是有连接的 UDP是无连接
  2. TCP是可靠传输 UDP是不可靠传输
  3. TCP是面向字节流 UDP是面向数据报
  4. TCP和 UDP都是全双工

连接:通信双方是否会记录保存对端的信息。

UDP:每次发送数据都需要手动在send方法中指定

TCP:连接如何建立,不需要代码干预,由系统内核自动完成

对应用程序来说:

  • 客户端主要是发起“建立连接”的动作

  • 服务器主要是把建立好的连接从内核中拿到应用程序里

如果有客户端和服务器要建立连接,这时服务器的应用程序是没有任何感知的,内核直接就完成了建立连接的流程(三次握手),完成流程之后,就会在内核的队列中(每一个ServerSocket都有这样的一个队列)排队。

应用程序要想和客户端进行通信,就需要通过一个accept方法,把内核中已经建立好的连接对象,拿到应用程序中

2.2代码示例

2.2.1代码
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 severSocket = null;

	public TcpEchoServer(int port) throws IOException {
		severSocket = new ServerSocket(port);
	}
	public void start() throws IOException {
		System.out.println("服务器启动");
		ExecutorService service = Executors.newCachedThreadPool();
		while(true){
			//通过accetp方法,把内核中已经建立 好的链接拿到应用中
			//建立连接的细节流程是内核自动完成的,应用程序只需要捡现成的即可
			Socket clientSocket = severSocket.accept();
			service.submit(new Runnable(){
				@Override
				public void run(){
					processConnection(clientSocket);
				}
			});
		}
	}

	//通过这个方法来处理当前的连接
	public void processConnection(Socket clientSocket){
		//进入方法,打印出一个日志,表示当前有客户端连接上了
		System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
		try(InputStream inputStream = clientSocket.getInputStream();
			OutputStream outputStream = clientSocket.getOutputStream()) {
			// 使用 try ( ) 方式, 避免后续用完了流对象, 忘记关闭.
			// 由于客户端发来的数据, 可能是 "多条数据", 针对多条数据, 就循环的处理.
			while(true){
				Scanner sc = new Scanner(inputStream);
				if(!sc.hasNext()){
					// 连接断开了. 此时循环就应该结束
					System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
					break;
				}
				// 1. 读取请求并解析. 此处就以 next 来作为读取请求的方式. next 的规则是, 读到 "空白符" 就返回.
				String request = sc.next();
				// 2. 根据请求, 计算响应.
				String response = processs(request);
				// 3. 把响应写回到客户端.
				//    可以把 String 转成字节数组, 写入到 OutputStream
				//    也可以使用 PrintWriter 把 OutputStream 包裹一下, 来写入字符串.
				PrintWriter printWriter = new PrintWriter(outputStream);
				//    此处的 println 不是打印到控制台了, 而是写入到 outputStream 对应的流对象中, 也就是写入到 clientSocket 里面.
				//    自然这个数据也就通过网络发送出去了. (发给当前这个连接的另外一端)
				//    此处使用 println 带有 \n 也是为了后续 客户端这边 可以使用 scanner.next 来读取数据.
				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) {
			throw new RuntimeException(e);
		}finally{
			try {
				// 在这个地方, 进行 clientSocket 的关闭.
				// processConnection 就是在处理一个连接. 这个方法执行完毕, 这个连接也就处理完了.
				clientSocket.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}

	}

	public String processs(String request){
		return request;
	}

	public static void main(String[] args) throws IOException {
		TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
		tcpEchoServer.start();
	}
}
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 {
		// 需要在创建 Socket 的同时, 和服务器 "建立连接", 此时就得告诉 Socket 服务器在哪里~~
		// 具体建立连接的细节, 不需要咱们代码手动干预. 是内核自动负责的.
		// 当我们 new 这个对象的时候, 操作系统内核, 就开始进行 三次握手 具体细节, 完成建立连接的过程了.
		socket = new Socket(serverIp,ServerPort);
	}
	public void start(){
		Scanner sc = new Scanner(System.in);
		try(InputStream inputStream = socket.getInputStream();
			OutputStream outputStream = socket.getOutputStream()) {
			PrintWriter writer = new PrintWriter(outputStream);
			Scanner scannerNetWork = new Scanner(inputStream);
			while(true){
				//1.从控制台读取用户输入的内容
				System.out.println("->");
				String request = sc.next();
				// 2. 把字符串作为请求, 发送给服务器
				//    这里使用 println, 是为了让请求后面带上换行.
				//    也就是和服务器读取请求, scanner.next 呼应
				writer.println(request);
				writer.flush();
				//3.读取服务器返回的相应
				String response = scannerNetWork.next();
				//4.输出内容
				System.out.println(response);
			}

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws IOException {
		TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
		tcpEchoClient.start();
	}
}
2.2.2 注意事项
clientSocket需要进行close操作
Socket clientSocket

在UDP中的DatagramSocket和ServerSocket都没有写close,那为什么没问题呢?

因为UDP中的DatagramSocket和ServerSocket的周期是贯穿着整个程序的,只有这么一个对象,所以并不会频繁的创建,因此不会造成内存泄漏问题。

但是TCP中,ClientSocket是在一个循环中,每当有新的客户端来建立连接就会创建出一个新的ClienbtSocket

虽然我们已经在下面进行了trycatch操作,但这里关闭的知识clinetSocker上自带的流对象,并没有关闭socket本身

image-20231022121135160

所以,我们需要在代码中,通过finally加上close,来确保socket能被正确的关闭

image-20231022121910772

使用线程池来建立多端连接

为什么我们需要线程池来处理多端连接问题呢?

image-20231022130700601

如图所示,图上的代码并没有使用线程池,接下来让我们分析一下问题

第一个客户端连接之后,accept就返回了,得到了一个clientSocket,进入了processConnection

接下来又进入了一个while循环,在这个循环中,需要反复处理客户端发来的请求数据,如果客户端这会还没有发送请求,服务器代码就会阻塞在sc.hasNext这里

此时此刻,第二个客户端来连接了,此时是可以连接成功的(因为连接由内核负责)建立连接之后,连接对象就会在内核的队列里等待,直到代码通过accept把连接取出来。

但是当前的代码是无法第一时间执行到第二次的accept。第一个客户端会使服务器处于processConnection内部,直到第一个客户端退出,processConnection才能结束,才能执行后续的操作。

出现这个问题的关键点在于两重循环在一个线程里,进入第二重循环的时候,无法继续执行第一个循环,UDP服务器中只有一个循环,故不会出现这个问题,所以我们应使用多线程来解决。

image-20231022131028252

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司徒阿宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值