网络编程:通过UDP和TCP分别实现回显服务器

目录

网络编程

封装和分用

通过Socket API进行应用层和传输层之间的交互

//TCP和UDP的区别

网络通信数据的基本单位:

UDP的socket api如何使用

【1】DatagramSocket

【2】DatagramPacket

【服务器端代码】

【客户端代码】

TCP的socket api

【1】ServerSocket

【2】Socket

【服务器端代码】

【客户端代码】


网络编程

IP地址:使用一个32位4字节数字表示地址(一般来说会把IP地址给表示成4个0-255之间的十进制数字,并使用3个点进行分隔)

端口号:区分一个主机上不同的应用程序,端口号是一个整数(2个字节,0-65535)(一个端口号只能被一个程序绑定,但是一个程序可以绑定多个端口)

//0一般不使用,1-1023这个范围的端口号系统留作特殊用途,写的程序不应该占用

封装和分用

描述了网络通信过程中基本的数据传输流程

//一个数据报=报头+载荷(字符串拼接)

1.应用层:根据约定的应用层协议来生成应用层数据报,通过操作系统的api把数据交给传输层(具体是用几个字段,字段的顺序如何,使用什么符合分隔都是可以灵活调整的)

2.传输层:在应用层数据报的基础上拼接上传输层的报头,变成传输层的数据报(传输层典型的协议,TUP,UDP)(包含源端口和目的端口),传输层数据报搞好之后这个数据又会进一步交给网络层

3.网络层

网络层最主要的协议是IP协议(包含源IP和目的IP)

再交给数据链路层进一步打包

4.数据链路层

最主要的协议是以太网(报头中包含源mac地址和目的mac地址)//此处会比之前多一个加报尾的操作

5.物理层

把上述数据转化成2进制的01序列后通过光信号/电信号进行传输

//数据发送出去之后就会经过一系列的交换机和路由器进行转发,A和B一般来说不是直接网线连接的,中间还要经过很多的交换机/路由器设备进行转发,当数据到达B这边之后,B就要针对上述数据进行"分用"(针对上述数据报进行层层的解析)

数据报在网络中间还会经历一定的转发过程;如果经过路由器就会封装分用到网络层,路由器解析到网络层拿到IP地址再决定进一步如何运输,下一步传输的时候又会重新经过数据链路层和物理层的封装;如果经过交换机就会封装分用到数据链路层

通过Socket API进行应用层和传输层之间的交互

传输层提供的网络协议主要是TCP和UDP,这两个协议的特性(工作原理)差异很大,导致使用这两种协议进行网络编程时也存在一定差别,所以系统分别提供了两套API

//TCP和UDP的区别

(1)TCP是有连接的,UDP是无连接的

(连接是抽象的概念,计算机中这种抽象的连接是很常见的,此处的连接本质上是建立连接的双方各自保存对方的信息)

//TCP要想通信就得先建立连接(得对方同意才能通信),UDP想要通信直接发送数据即可(UDP不保存对方的信息,但是我们调用UDP的socket api的时候要把对方的位置啥的给传过去)

(2)TCP是可靠传输的,UDP是不可靠传输的

//可靠传输指的是A在传送消息失败时能感知到,就可以在发送失败的时候采取一定的措施(尝试重传之类的)可靠传输的代价是机制更复杂以及传输效率会降低

(3)TCP是面向字节流的,UDP是面向数据报

(4)TCP和UDP都是全双工的

//一个信道允许双向通信就是全双工(代码中使用一个socket对象就可以发送数据也能接受数据),单向通信就是半双工

网络通信数据的基本单位:

【1】数据报(Datagram)【2】数据包(Packet)【3】数据帧(Frame)【4】数据段(Segment)

UDP的socket api如何使用

【1】DatagramSocket

void receive(DatagramPacket p)

void send(DatagramPacket p)

void close()

//socket本质上是一种特殊的文件,属于是把“网卡”这个设备抽象成了文件,做到了把网络通信和文件操作给统一了

【2】DatagramPacket

创建时得指定一块内存空间DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);

//使用这个类来表示一个UDP数据报,UDP是面向数据报的,每次进行传输都要以UDP数据报作为基本单位

服务器和客户端都需要创建socket对象,但是服务器的socket一般要显式的指定一个端口号,而客户端的socket一般不能显式指定(不显式指定,此时系统会自动分配一个随机的端口)

【服务器端代码】

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 {
    //这么写就是手动指定端口
        socket=new DatagramSocket(port);
         //这么写就是系统自动分配端口
         //socket=new DatagramSocket();
    }
    public void start() throws IOException {
        //通过这个方法来启动服务器
        System.out.println("服务器启动");
        //一个服务器程序中,经常能看到while true这样的代码
        while(true){
          //1.读取请求并解析
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            //当前完成receive之后,数据是以二进制的形式存储到DatagramPacket中了
            //要想能够把这里的数据给显示出来,还需要把这个二进制数据给转成字符串
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());
         //2.根据请求计算响应(一般的服务器都会经历的过程)
            //由于此处是回显服务器,请求是啥样,响应就是啥样
            String response=process(request);
         //3.把响应写回到客户端
            //搞一个响应对象DatagramPacket,往DatagramPacket里构造刚才的数据,再通过send返回
            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(),requese,response);
        }
    }
    public String process(String request){
        return request;
    }
    public static void main(String[] args) throws IOException{
        UdpEchoServer server=new UdpEchoServer(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 UdpEchoClient{
    private DatagramSocket socket=null;
    private String serverIp="";
    private int serverPort=0;
    public UdpEchoClient(String ip,int port) throws SocketException {
        //创建这个对象时不能手动指定端口
        socket=new DatagramSocket();
        //由于UDP自身不会持有对端的信息,就需要在应用程序里把对端的情况给记录下来
        //这里主要记录对端的ip和端口
        serverIp=ip;
        serverPort=port;
    }
    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner=new Scanner(System.in);
        while(true){
            //1.从控制台读取数据作为请求
            System.out.print("->");
            String request=scanner.next();
            //2.把请求内容构造出DatagramPacket对象发给服务器
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),serverPort);
            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 client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

TCP的socket api

(TCP是字节流的,传输的基本单位是byte)

【1】ServerSocket

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

【2】Socket

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

【服务器端代码】

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 serverSocket=null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException{
        System.out.println("服务器启动!");
        ExecutorService service= Executors.newCachedThreadPool();
        while(true){
            //通过accept方法,把内核中已经建立好的连接拿到应用程序中
            //建立连接的细节流程都是内核自动完成的,应用程序只需要“捡现成”的
            Socket clientSocket=serverSocket.accept();
            //此处不应该直接调用processConnection,会导致服务器不能处理多个客户端
            //创建新的线程来用是更合理的做法
            // 这种做法可行,但是不够好
           // Thread t=new Thread(()->{
           //processConnection(clientSocket);
           //});
           //t.start();
           //更好一点的办法是使用线程池
            service.submit(new Runnable(){
                @Override
                public void run(){
                    processConnection(clientSocket);
                }
            });
           //还有一些其他手段可以来处理线程较多的情况:协程、IO多路复用、IO多路转接
        }
    }
    //通过这个方法来处理当前的连接
    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 scanner=new Scanner(inputStream);
                if(!scanner.hasNext()){
                //连接断开了循环就应该结束
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析,此处就以next来作为读取请求的方式,next的规则就是读到“空白符”就返回
                String request=scanner.next();
                //2.根据请求计算响应
                String response=process(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){
            e.printStackTrace();
        }finally{
            try{
                //在这个地方进行clientSocket的关闭
                //processConnection就是在处理一个连接,这个方法执行完毕,这个连接也就处理完了
                clientSocket.close();
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
    public String process(String request){
       //此处也是写的回显服务器,响应和请求是一样的
        return request;
    }
    public static void main(String[] args) throws IOException{
        TcpEchoServer server=new TcpEchoServer(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 TcpEchoClient{
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
    //需要在创建Socket的同时和服务器“建立连接”,此时就得告诉Socket服务器在哪里
    //具体建立连接的细节不需要利用代码手动干预,是内核自动负责的
    //当new这个对象的时候,操作系统内核就开始进行“三次握手”具体细节完成建立连接的过程
        socket=new Socket(serverIp,serverPort);
    }
    public void start(){
    //tcp的客户端行为和udp的客户端差不多
        Scanner scanner=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.print("-> ");
                String request=scanner.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 client=new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

注意:

在进行运行测试时应该先启动服务器再启动客户端

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值