网络编程基础(socket)

目录

1. TCP和UDP

2. TCP特点与UDP特点对比

2.1 TCP特点

2.2 UDP特点

3. UDP socket 相关类

3.1 回显服务器(UDP)

 3.2 翻译服务器(UDP)

4. TCP socket 相关类

4.1 回显服务器(TCP)

4.2 翻译服务器(TCP)


1. TCP和UDP

  • 进行网络编程我们就需要用到socket套接字,这是操作系统给应用程序提供的API,就是应用层给传输层提供的,socket属于传输层。
  • 在网络传输层中有很多协议,最典型的就是TCP和UDP。由于这两种协议差异很大,所以操作系统提供了两个版本的API。

2. TCP特点与UDP特点对比

2.1 TCP特点

  • 有连接;打电话就是有连接,发微信就是无连接,需要通信双方建立连接才能说话就是有连接。
  • 可靠传输;可靠传输就比如打电话时对方回应你能知到对方收到信息。
  • 面向字节流;文件操作的时候就是 流 ,TCP和文件操作一样,也是基于流的。
  • 全双工。一个通道双向通信,对应的半双工就是一个通道单向通信。

2.2 UDP特点

  • 无连接;
  • 不可靠传输;
  • 面向数据报;以数据报为基本单位。
  • 全双工。

3. UDP socket 相关类

  • DatagramSocket:

构造方法及其方法类似于文件操作,socket本质也是文件,广义的文件也就是各种硬件设备和软件资源,socket对应的就是网卡这个硬件设备,通过网卡发送数据就是写文件,通过网卡接收文件就是读文件。

  • DatagramPacket(UDP数据报,也就是一次发送/接收的基本单位)

DatagramPacket构造方式:

①构造空的packet,不需要指定端口;

DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);

②构造有数据的packet,使用InetAddress描述地址和端口号

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());

③构造有数据的packet,使用ip和端口号描述发给谁

DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(this.serverIP), this.serverPort);

3.1 回显服务器(UDP)

UDP版本的客户端服务器程序:回显服务器(发出什么收回什么,没有任何意义,仅仅用来了解socket api的使用)。

回显服务器整个流程(没有数据就会阻塞,有数据才会继续执行):

服务器启动;构造socket.receive()进入循环立即执行,由于客户端还没有数据发来,暂时阻塞

→客户端启动;构造socket对象,读取用户数据;构造packet并且发送(socket.send);同时socket.receive就会解除阻塞

→服务器执行process方法,根据请求计算响应;socket.send发送响应给客户端

→客户端也会socket.receive,此时服务器的响应还没回来,receive也会阻塞;

客户端收到数据才会从receive中被唤醒,并且拿到响应数据;将数据展示到界面上

→服务器进入到下次循环,继续在socket.receive阻塞

服务器:

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

public class UdpEchoServer {
    private DatagramSocket socket = null;

    // 服务器的构造方法,参数的端口表示服务器要绑定的端口
    // 这里的端口是为了让客户端能够找到,服务器不担心端口冲突是因为服务器是程自己开发,能够确定端口是否空闲
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    // 启动服务器的方法
    public void start() throws IOException {
        System.out.println("服务器启动!");
        // 由于服务器不知道客户端什么时候发送请求,就使用循环随时在线
        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.把响应写回到客户端
            // 使用带有数据的内存空间进行构造,requestPacket.getSocketAddress()就是客户端的ip和端口号,用一个对象来表示了(使用带有数据的内存空间来构造)
            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 request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        // 端口是一个16位的整数,0-65535,但是0-1024称为“知名端口号”,被一些较为著名的应用程序占用,1024-65535是可以随意使用的端口号
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

客户端:

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    // 客户端的构造方法,参数是传一个服务器的IP和服务器的端口
    // 两个参数一会会在发送数据的时候用到.暂时先把这俩参数存起来
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 客户端给服务器发送一个请求,客户端用自己的主机ip,就是源ip,不用自己绑定一个端口,操作系统会自动分配一个空闲的端口。
        // 不确定端口是否空闲,避免端口冲突
        socket = new DatagramSocket();
        // 假设 serverIP 是形如 1.2.3.4 这种点分十进制的表示方式 (常用的IP表现方式)
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取用户输入的内容.
            System.out.print("请输入: ");
            String request = scanner.next();
            // 2. 构造一个 UDP 请求, 发送给服务器.
            // request.getBytes()把字符串转换成字节数组,此处需要以字节为单位描述,这就是不用request.length的原因
            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);
            // 调用receive的时候,当前数据还未到达,receive就会阻塞,直到数据到达
            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();
    }
}

允许启动多个客户端:

运行结果展示:

​​​​

 3.2 翻译服务器(UDP)

我们上面实现的回显服务器没有什么作用,下面实现一个英汉互译的翻译服务器。

逻辑类似于回显服务器,只有process(根据请求计算响应)不同。

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

// 翻译服务器,基于回显服务器
public class UdpTranslateServer extends UdpEchoServer {
    // 翻译的本质上就是 key -> value
    private Map<String, String> dict = new HashMap<>();

    public UdpTranslateServer(int port) throws SocketException {
        super(port);

        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("hello","你好");
        // 在这里就可以填入很多很多的内容. 翻译词典就是有一个非常大的哈希表, 包含了几十万个单词.
    }

    // 重写 process 方法, 实现查询哈希表的操作
    @Override
    public String process(String request) {
        return dict.getOrDefault(request, "词在词典中未找到");
    }

    // start 方法和父类完全一样, 不用写了.

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

 服务器程序的基本流程相似,核心就是根据请求计算响应。

4. TCP socket 相关类

  • ServerSocket:给服务器端使用的类

构造方法:ServerSocket(int port):绑定端口,监听端口

其他方法:①Socket accept():接受客户端连接;②void close():关闭套接字。

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

构造方法:Socket(String host,int port):尝试和指定的服务器建立连接,传输数据

其他方法:①通过socket可以获取两个流对象进行读和写:InputStream getInputStream()、OutputStream getOutputStream() ;②InetAddress getInetAddress()返回套接字获取到对方的IP地址和端口。

4.1 回显服务器(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("服务器启动!");
        while (true) {
            // 1. 先调用 accept 来接受客户端的连接.
            // 如果当前没有客户端连接accept就会阻塞
            Socket clientSocket = listenSocket.accept();
            // 2. 再处理这个连接.
            processConnection(clientSocket);
        }
    }

    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 scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 读完了, 连接可以断开了.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回给客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                // 刷新缓冲区确保数据确实是通过网卡发送出去了.
                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 ? 而前面的 listenSocket 以及 udp 程序中的 socket 没有 close?
            // 由于socket也是文件,文件最后一定需要关闭释放。
            // listenSocket是在TCP服务器程序中的的唯一对象,不可能占满描述符表,随着进程的退出会自动释放。
            // 而clientSocket是在循环中,每创建一个实例都需要消耗一个文件描述符表,因此需要及时释放不用的clientSocket。
            clientSocket.close();
        }
    }

    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 {
    // 客户端需要使用这个 socket 对象来建立连接.
    private Socket socket = null;

    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        // 和服务器建立连接. 就需要知道服务器在哪.
        socket = new Socket(serverIP, serverPort);
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        // Scanner不需要关闭是因为本质上是关闭了里面包含的InputStream,
        // scanner自身不会打开文件描述,且InputStream只有一个对象,且在try里面,最终会自己结束
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 从控制台读取数据, 构造成一个请求
                System.out.print("请输入:");
                String request = scanner.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) {
            e.printStackTrace();
        }
    }

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

但是我们的服务器是需要与多个客户端建立连接,如果没有客户端建立连接,服务器就会阻塞到accept,如果有客户端建立连接,此时就会进入processConnection方法,建立连接了,但是客户端还没有发消息,此时就会在scanner.hasNext()阻塞,此时就无法第二次调用到accept,也就无法处理第二个客户端了。

UDP没有这个问题的原因是客户端直接发消息即可,而TCP是一个连接处理多次请求,无法快速调用accept(长连接),若TCP每个连接只处理一个客户端请求就不会出现这种问题(短连接)。

while (true) {
                // 1. 读取请求并解析.
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 读完了, 连接可以断开了.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }

改进方案:使用多线程可以解决上述存在的问题,每个客户端发出请求都能连接到新的线程。

服务器端改进(多线程):

public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 1. 先调用 accept 来接受客户端的连接.
            // 如果当前没有客户端连接accept就会阻塞
            Socket clientSocket = listenSocket.accept();
            // 2. 再处理这个连接. 这里应该要使用多线程. 每个客户端连上来都分配一个新的线程负责处理
            Thread t = new Thread(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }

但是上述循环会循环多次,频繁创建销毁线程,可以使用线程池解决这个问题。

服务器端改进(线程池):

public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
            // 1. 先调用 accept 来接受客户端的连接.
            // 如果当前没有客户端连接accept就会阻塞
            Socket clientSocket = listenSocket.accept();
            // 2. 再处理这个连接. 这里应该要使用多线程. 每个客户端连上来都分配一个新的线程负责处理
            //    此处使用多线程可以解决问题, 但是会导致频繁创建销毁多次线程!!
//            Thread t = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();

            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

4.2 翻译服务器(TCP)

// 翻译服务器,基于回显服务器
public class TcpTranslateServer extends TcpEchoServer{
    // 定义一个字典
    private Map<String, String> dict = new HashMap<>();

    public TcpTranslateServer(int port) throws IOException {
        super(port);
        // 构造字典的内容
        dict.put("cat", "猫");
        dict.put("dog", "狗");
        dict.put("pig", "猪");
        dict.put("hello", "你好");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request,"没有这个单词!");
    }

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值