什么是网络编程
网络编程指的是,网络上的主机,不同的进程,通过网络的方式实现通信。同一个主机但是不同进程之间通信也是网络编程,但是我们主要编程对象是不同主机至今啊的通信。
网络编程实际上就是把传输层和应用层进行封装,然后利用java提供的api进行通过,代码的形式交给传输层然后进行通信。
网络编程的基本概念
客户端和服务器
- 客户端:发起通信的一方,实际上就是我们平时的应用之类的。
- 服务器:接受数据的一方,接收数据并进行处理。
客户端服务器的定义实际上是谁发起了通信,谁接受了数据。
请求和响应
请求:request,客户端给服务器发送的数据。
响应:response,服务器返回给客户端的数据。
Socket关键字
Socket是系统提供的方式用于网络通信,网络通信常常基于Socket关键字。
TCP和UDP是传输层的两个重要的协议。
TCP的特点
- 有连接(必须要双方都接通了才能进行通信,需要三次握手四次挥手)。
- 可靠传输(可以知道对方是否接收到了数据)。
- 面向字节流(网络中的传输数据是字节模式,以字节为单位)。
- 全双工(可以双向通信)。
UDP的特点
- 无连接(就是类似于QQ直接发出去,无需等待对方建立连接)。
- 不可靠传输(对方就算对方没有接收到,发送端也不知道有没有对方是否接收到)。
- 面向数据报(单位是数据报)。
- 全双工(可以双向通信)。
UDP编程:
1.DatagramSocket
Datagramsocket是UDP Socket的关键方法,用来发送和接受UDP数据。
构造方法:
![]()
重要方法:
![]()
2.DatagramPacket
DatagramPacket是UDP Socket发送数据的数据报(每次接收和发送数据的基本单位就是 数据报)。
构造方法:
![]()
![]()
3.UDP回显服务器
服务器和客户端都要指定一个端口号,但是一般服务器的端口号要显式指定,客户端不能显式指定,系统会自动分配。服务器需要把端口号明确下来,需要让别人找到。客户端的端口号不能指定,因为有可能被别人占用了(避免端口号冲突),交给系统分配。
服务器的端口号在程序员手里,服务器的哪些端口号被使用了,程序员都知道的。客户端在客户上面。一个服务器程序需要长时间运行。
new DatagramPacket()用来承载从网卡这边读到的数据,读到数据需要指定一个内存空间来保存这个数据。socket(网卡)读取数据,,并且保存到requestpacket里面。
receive会阻塞,直到客户端发送数据。
- package network;
-
- import java.io.IOException;
- import java.net.DatagramPacket;
- import java.net.DatagramSocket;
- import java.net.SocketException;
-
- public class UdpEchoServer {
- //服务器代码,创建一个DatagramSocket,后续操作网卡
-
- private DatagramSocket socket=null;
-
- public UdpEchoServer(int port) throws SocketException {
- this.socket = new DatagramSocket(port);
-
- //socket=new DatagramSocket();这是让系统分配的方法。
-
-
-
-
- }
- public void start() throws IOException {
- while (true){
- //读取请求并且解析
- DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
- //从网卡中读取数据,并且存储在packet中,有数据才会接受不然就会阻塞
- socket.receive(requestPacket);
- //拿到数据,并且放入到String中,取区间内的字节,构造成String,这里的getlength实际上不是4096,是收到的数据的真实长度
- String request=new String(requestPacket.getData(),0, requestPacket.getLength());
- //根据请求计算响应!!!!一般服务器最重要的
- String response=process(request);
-
-
- //将响应写回去
- //UDP是无连接的,每次都要指定数据要发给谁
- //构造数据报,需要指定数据内容,也要指定发给谁
- //不能直接getlength,获取字符为单位的如果都是英文单词,那字符字节一样,中午不一样,网络传输都是字节为单位
- DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
- socket.send(responsePacket);
-
-
-
-
-
- System.out.printf("[%s:%d] req=%s resp=%s",requestPacket.getAddress().toString(),responsePacket.getPort(),request,response);
-
- }
-
-
-
- }
-
-
- private String process(String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
- udpEchoServer.start();
-
-
-
-
- }
- }
对于客户端,服务器的端口号可以由系统随机分配,但需要知道服务器的IP地址及端口号,不然就不知道发送数据给谁。
- 客户端发送数据
- 构造数据报通过socket发送给服务器
- 服务器进行读取并且返回给客户端
- 客户端输出发送的响应
- package network;
-
- import java.io.IOException;
- import java.net.*;
- import java.util.Scanner;
-
- public class UdpEchoClient {
- private DatagramSocket socket;
- private String address;
- private int port;
-
- public UdpEchoClient(String address, int port) throws SocketException {
- this.address = address;
- this.port = port;
- socket=new DatagramSocket();//这里表示服务器的随机端口创建
- }
- public void start() throws IOException {
- System.out.println("客户端启动");
- Scanner input=new Scanner(System.in);
- while (true){
- //没有输入数据的时候就跳出循环
- if(!input.hasNext()){
- break;
- }
- //读取所有的数据
- String request=input.next();
- //将内容构造成datagrampacket发送出去,并且需要找到对方的ip地址和端口号
- DatagramPacket datagramPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(address),port);
- //通过网卡进行发送数据报
- socket.send(datagramPacket);
-
- //读取服务器内容并且显示在客户端。
- DatagramPacket responseDatagramPacket=new DatagramPacket(new byte[4096],4096);
- socket.receive(responseDatagramPacket);
- String response=new String(responseDatagramPacket.getData(),0,responseDatagramPacket.getLength());
- System.out.println(response);
-
-
-
- }
- }
-
- public static void main(String[] args) throws IOException {
- UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9090);
- udpEchoClient.start();
- }
- }
![]()
![]()
但是实际上这个程序不能跨主机通信,如果想要实现跨主机通信,就要把程序部署到云服务器上面。
4.UDP翻译回显服务器
基于上述回显服务器,还可以实现出一些其他带有一点业务逻辑的服务器。
进行业务逻辑的修改实际上就是进行对回显服务器的继承,再实现更多的细节和代码。
上述的操作是在process的代码中实现的,我们只要进行继承然后重写方法就可以达到汉译英的效果了。
- package network;
-
- import java.io.IOException;
- import java.net.SocketException;
- import java.util.HashMap;
- import java.util.Map;
-
- public class UdpDireServer extends UdpEchoServer{
- private Map
map; - public UdpDireServer(int port) throws SocketException {
- super(port);
- map = new HashMap<>();
- map.put("cat", "小猫");
- map.put("bear", "小熊");
- }
-
- @Override
- public String process(String request) {
- if(map.get(request)!=null){
- return map.get(request);
- }
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- UdpDireServer udpDireServer=new UdpDireServer(9090);
- udpDireServer.start();
- }
- }
观察一下运行结果,发现没有问题。
![]()
以上就是UDP的回显服务器的开发和运行了。
TCP编程:
1.ServerSocket
ServerSocket是创建TCP服务端Socket的API(只能给服务器使用)。
构造方法:
![]()
重要方法:
![]()
![]()
2.Socket
Socket 类用于创建客户端 Socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket. (服务器端和客户端都能使用)
构造方法:
![]()
重要方法:
![]()
3.TCP回显服务器程序
TCP和UDP的区别就是TCP是有连接的,就和打电话一样需要一方接通另外一方才能进行通话。所以要等待客户端发起请求后,服务器确认接通之后,才可以进行通信,TCP的首要任务就是建立连接。
和UDP回显服务器一样,对于这里的服务器,同样需要指定端口号创建TCP服务器端Socket,即ServerSocket。
- 服务器启动之后,需要通过accept方法来监听当前端口。
- 成功建立连接之后,就可以返回一个Socket方法,这个对象保存了对端的信息,即客户端信息,可以用来接收和发送请求等(TCP是面向字节流),可以通过该方法来发送和接收数据。
后续流程和UCP回显服务器一致。此处由于每有一个客户端连接,就会有一个clientSocket,这里消耗的Socket会越来越多,因此每当一个客户端连接结束,就需要释放这个clientSocket。
- 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.time.temporal.IsoFields;
- import java.util.Scanner;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class TcpEchoServer {
- private ServerSocket serverSocket;
-
- public TcpEchoServer(int port) throws IOException {
- serverSocket=new ServerSocket(port);
- }
-
-
- public void start() throws IOException {
- System.out.println("服务器启动");
- //ExecutorService threadPool = Executors.newCachedThreadPool();
- while (true){
- while (true) {
- //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
- Socket clientSocket = serverSocket.accept();
- processConnection(clientSocket);
- }
-
-
- }
- }
-
- private void processConnection(Socket clientSocket) throws IOException {
- //返回ip地址和对应的端口
- System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());
- try(OutputStream outputStream=clientSocket.getOutputStream();
- InputStream inputStream=clientSocket.getInputStream()) {
- //不断读取输入的数据
- while (true){
- Scanner scanner=new Scanner(inputStream);
- if (!scanner.hasNext()) {
- System.out.printf("[%s:%d] 客户端下线\n",
- clientSocket.getInetAddress(), clientSocket.getPort());
- break;
- }
- //next是等到/n才结束,也就是用户输入换行(回车)
- String request=scanner.next();
- String response=process(request);
-
- PrintWriter printWriter=new PrintWriter(outputStream);
- printWriter.println(response);
- printWriter.flush();
- System.out.printf("[%s:%d] request:%s response:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
-
- }
-
- }finally {
- clientSocket.close();
- }
- }
-
- private String process (String request) {
- return request;
- }
-
- public static void main(String[] args) throws IOException {
- TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
- tcpEchoServer.start();
-
- }
- }
对于客户端,需要指定服务器的IP和端口号建立连接。使用 Socket(String host, int port) 创建Socket的时候,就开始发起与对应服务器建立连接的请求了。
实际上TCP回显服务器和UDP很相似。,但是TCP是面向字节流。
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.io.PrintWriter;
- import java.net.InetAddress;
- import java.net.Socket;
- import java.net.UnknownHostException;
- import java.util.Scanner;
-
- public class TcpEchoClient {
- private Socket clientSocket;
-
- public TcpEchoClient(String serverAddress, int serverPort) throws IOException {
- clientSocket=new Socket(InetAddress.getByName(serverAddress),serverPort);
- }
- public void start() {
- try(InputStream inputStream=clientSocket.getInputStream();
- OutputStream outputStream=clientSocket.getOutputStream();
- Scanner scanner=new Scanner(System.in)) {
- while (true){
- System.out.print("->");
- String request=scanner.next();
-
- PrintWriter printWriter=new PrintWriter(outputStream);
- printWriter.println(request);
- printWriter.flush();
-
- //读取服务器发送的数据
- Scanner inputScanner=new Scanner(inputStream);
- String response=inputScanner.next();
-
- System.out.println(response);
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- public static void main(String[] args) throws IOException {
- TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
- tcpEchoClient.start();
- }
-
-
- }
在这里我们需要先运行服务器,再运行客户端,通常服务器都需要先启动,不然客户端会因为连接不上服务器而报错。
![]()
![]()
实际上这里还存在一个问题。这里的服务器只能给先获取连接的客户端提供服务,如果其他客户端想访问则会失败。
分析过程:
- 第一个客户端连上服务器之后,服务器就会从accept这里返回(解除阻塞),然后进入到processConnection方法中。
.接下来服务器就会在processConnection循环处理客户端的请求,只有当客户端退出之后,连接结束,才会退出循环。 - 而服务器在循环处理客户端请求的时候,第二个客户端发起连接请求,而服务器这里并不能执行到accept。因此并不能成功连接,只有当客户端退出,才会执行回到accept进行连接。
第二个客户端之前发的请求为什么能被立即处理?
- 当前TCP在内核中,每个 socket 都是有缓冲区的。客户端发送的数据通过客户端代码,已经写入到服务器的缓冲区了,这里数据确实发送了,只不过数据在服务器的接收缓冲区中。
- 一旦第一个客户端退出,回到第一层循环,执行accept连接操作,后续processConnection方法里的 next 就能把之前缓冲区的内容给读出来。
实际上可以通过多线程来解决此问题,为每个访问的客户端都创建一个线程,使其可以通过单独的线程来进行访问服务器。
4. 服务器引入多线程
- //多线程
- public void start() throws IOException {
- System.out.println("服务器启动!");
- while (true) {
- //监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息
- Socket clientSocket = serverSocket.accept();
-
- Thread t = new Thread(() -> {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- t.start();
- }
- }
但是实际上,像这样频繁创建和销毁线程对服务器来说是一个不小的开销。
- 每有一个客户端连接,就会创建一个新的线程,每当这个客户端结束,就要销毁这个线程。
- 如果客户端比较多,并且频繁连接、关闭,就会使服务器频繁创建和销毁线程
因此我们使用了线程池。
5.服务器引入线程池
- public void start() throws IOException {
- System.out.println("服务器启动!");
- ExecutorService threadPool = Executors.newCachedThreadPool();
- while (true) {
- Socket clientSocket = serverSocket.accept();
-
- threadPool.submit(new Runnable() {
- @Override
- public void run() {
- try {
- processConnection(clientSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- }
- }
6.TCP字典服务器
- import java.io.IOException;
- import java.util.HashMap;
- import java.util.Map;
-
- public class TcpDictServer extends TcpEchoServer{
- Map
map=new HashMap<>(); -
-
- public TcpDictServer(int port) throws IOException {
- super(port);
- map.put("cat","小猫");
-
-
- }
-
- @Override
- public String process(String request) {
- return map.getOrDefault(request,"未查找到单词");
- }
-
- public static void main(String[] args) throws IOException {
- TcpDictServer tcpDictServer=new TcpDictServer(9090);
- tcpDictServer.start();
- }
- }
![]()
评论记录:
回复评论: