网络编程之TCP

文章详细介绍了TCP(SocketAPI)中的ServerSocket和Socket类在服务端和客户端的使用,包括建立连接、读写数据以及实现回显服务器的过程。讨论了单线程、多线程及线程池版本的服务端实现,强调了多线程对于处理多个客户端请求的重要性,并提及了短连接与长连接的概念。此外,还提到了IO多路复用技术在高并发场景下的作用,对比了TCP与UDP的区别。
摘要由CSDN通过智能技术生成

TCP (socket api)

TCP提供两个类:
TCP是面相字节流;并不需要像UDP专门一个类构建数据报;是以字节的方式传输。(TCP : 有连接 可靠传输 面向字节流 全双工)

ServerSocket

ServerSocket 是一个在服务器端等待监听来自客户端的连接请求的类。当客户端请求建立连接时,ServerSocket 对象将接受连接请求并创建一个新的 Socket 对象,该对象将与远程主机建立连接。服务器端可以使用连接的 Socket 对象向客户端发送和接收数据。

ServerSocket:给服务器使用的Socket对象
构造方法:
在这里插入图片描述
普通方法:
在这里插入图片描述
这个方法相当于接电话;建立连接之后会返回一个Socket对象;我们通过这个对象与客户端进行沟通。

Socket

Socket 是用于建立客户端和服务器之间的连接。客户端使用 Socket 对象来与服务器通信。它提供了一组方法,可以使用TCP或UDP协议远程主机进行通信。
Socket:客户端,服务器都有使用的对象
构造方法
在这里插入图片描述

在这里插入图片描述

通过刚才简单的介绍这两个类和一些方法;进行一个回显服务器TCP版本体验网络通信的这个流程。

服务端实现

import java.io.IOException;//TCP的学习
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 test1 {
        public ServerSocket socket1=null;
            public test1(int port) throws IOException {//构建这样一个对象;传入一个端口号;因为是本机测试;所以我们在客户端就直接填环回IP
               socket1=new ServerSocket(port);//服务器的端口号
            }
        public void start() throws IOException {
                    System.out.println("服务器启动");
                    while (true) {
                        //接收连接accept方法;如果没有客户端建立连接;这个方法就会阻塞.
                        //建立连接后会返回一个socket对象;我们通过这个对象与客户端交互
                        Socket socket2 = socket1.accept();
                        //两个对象区别;socket1就像中介,通过accept方法把你介绍到socket2,然后由socket2再一顿操作。
                        //随后就处理这个介绍来的socket2;这个对象会占用文件描述符表,用完需要释放(量多、生命周期短)
                        processconnection(socket2);
                }
        }

    private void processconnection(Socket socket2) {
//通过介绍过来的对象可以创建输入、输出流对象;放try里能自动释放
        try (InputStream s2getinput= socket2.getInputStream();
             OutputStream s2output=socket2.getOutputStream()){
            Scanner scanner=new Scanner(s2getinput);//读取输入流
            while (true){//客户端可能输入123  然后234;多次请求;得要多次响应,就用循环。
            //1:读取请求;通过scanner读取
            if(!scanner.hasNext()){//当没有下一个数据;说明读完了(客户端关闭连接);使用next是读到空格、空白符结束;但是不返回这些
                System.out.println("客户端下线");
                break;

            }
            String request=scanner.next();
            //2:根据请求构造响应
            String response=process(request);

            //3:返回响应结果;正常就得利用OutputStream写回;但是这样有个问题;OutputStream不能直接写字符串
            //这里两种方案;把String的字节数组拿出来进行写入;把 s2output转化字符流
            
            //方案1
            //String responseData = "Hello, Client!";
            //s2output.write(responseData.getBytesUTF-8"));//转成字节数组再进行写入;
            
            //方案2(Reader、Writer是字符流输入输出)
			// Writer out = new OutputStreamWriter(s2output, "UTF-8");
			//out.write(responseData);
//================================================================之所以能直接写;网卡也是相当于文件IO的操作

            //方案2 PrintWriter 是一种字符流输出器,它继承自 Writer 类;子类功能更强大
            PrintWriter p=new PrintWriter(s2output);
            p.println(response);//使用println写入;带换行能方便接收;flush刷新一下,保证写入的确实发送出去
            p.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s \n",     socket1.getInetAddress().toString(),socket2.getPort(),request, response);
                

            }
            
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        finally {
            // 4:socket2回收这个对象
            // try并不是所有的都能使用当前能用但是其它情况可能并不能使用;更合适的做法, 是把 close 放到 finally 里面, 保证一定能够执行到!!
            try {
                socket2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {//回显服务器;直接简单的返回响应

                return request;
    }


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

总体步骤:
创建对象;建立连接;处理请求:getInputStream()把数据读出来、中间还有响应、getOutputStream()把响应数据写回去、关闭这个连接创建的对象。

我们在服务器创建的ServerSocket对象的作用是调用accept方法创建连接(在这里阻塞等待客户端发来连接请求);然后再换成操作这个新的accept返回的socket对象。
在这里插入图片描述

客户端

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 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便。
        // new 这个对象的同时, 就会进行 TCP 连接操作.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 先从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                // 2. 把读到的内容构造成请求, 发送给服务器.
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);//相当于又在写网卡;然后把数据发送出去
                // 此处加上 flush 保证数据确实发送出去了.
                printWriter.flush();
                // 3. 读取服务器的响应
                Scanner respScanner = new Scanner(inputStream);//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();
    }
}

对整个过程的执行步骤描述:
1:创建socket对象;先输出服务器启动;然后执行到 Socket socket2 = socket1.accept();阻塞等待连接。
2:客户端启动;创建连接 socket = new Socket(serverIp, serverPort);
3:服务器在获取连接后;准备处理连接;把客户端的内容读取出来。在scanner阻塞;
4:客户端在创建连接后继续往下执行;也是执行到scanner;阻塞等待用户输入。3和4的操作先后并不明确;但是也不影响。结果是两边都阻塞到这里
5:客户端两种可能;
一:输入exit/直接结束进程; 服务端Socket socket2 = socket1.accept()就获取不到客户端的socket;因为客户端这个已经下线。所以就没有这个输入流;那么scanner.hasnext也就返回false;就输出 客户端下线,并且结束本次的循环读取;等待下一次新的连接创建

二:正常输入;客户端进行往下直到阻塞scanner读取服务器的响应中。
6:服务器把输入的内容读取;然后再进行响应
7:服务器把响应的结果 通过PrintWriter写回去
8:客户端阻塞结束;读取服务器的响应;并且处理。
9:继续循环;等待你下一次的输入
(我们客户端服务器读取使用hasnext和循环的好处:能分多次输入和响应请求;如果输入 123 345 456.。。一次输入带有空格;就会读到空格为止是一次请求。回车也是如此。。因为这个读取一次读多少个都行;所以正好能用这个划分每次读取。使用next接收正好能识别这个换行符/空白符,并且内容不包含这个东西)

PrintWriter类的介绍:
我们使用PrintWriter写和OutputStream效果是一样的;往同一个地方写,对应的文件描述符表还是同一个,这里写文件就通过网卡发送。

细节补充:
为什么我们客户端、服务器在写的时候都带上\n呢,有什么好处?
在这里插入图片描述

TCP协议:面向字节流的协议;一次读多少个字节都可以;接收方怎么知道你这一次输入要读多少个字节?
所以\n就能做到为我们当前代码的请求和响应分割效果。

回车也是/n
我们客户端输入回车不就刚好给这个next低效了;但是我们发送过去的内容可没有回车;不然他会一直读取;不知道读多少个停止;所以有个/n就能刚好低效;next的结束标志。

多线程版本

上述的代码明显不合理;每次一个客户端的请求都得阻塞半天等它输入。我们的服务器肯定得要响应多个客户端;如果有其它客户端你不能让人家客户干等着。所以使用多线程客户端每发起一个请求就创建一个线程;单独的执行流;谁也不影响谁。(主要理解两个线程冲突在哪里发生占线问题)

在两个客户端下输入123然后回车并没有反应
在这里插入图片描述
直到我把test3的客户端结束后;test4客户端才能上线
在这里插入图片描述
问题出现在:两个线程;占线后;另一个线程都无法上线;
我们的代码相当于循环套循环;一个循环是为了接收多个客户端;另一个循环是为了多次读取和返回同一个客户端的请求、响应。问题在与
一个客户端连接上后;我们的代码跑到第2个循环卡住了;需要等你当前客户端结束后这个(结束客户端进程/输入exit)才能继续执行第一个循环尝试获取下一个连接。解决方案;在第二个循环使用多线程;我们的第一个循环每接收一个连接就创建一个新的请求处理、响应。让我们的代码光在第一个循环里走;一直循环有多少客户端就创建多少个连接。(简单来说就是第二个循环影响第一个循环的执行;accect无法第二次调用到;影响新客户端的连接)

需做如下改动
在这里插入图片描述

短连接与长连接:
短连接:客户端每次给服务器发消息就先建立连接;整体流程结束再关闭连接;下次再发送则重新建立连接
长连接:建立连接后;不着急断开;一系列的发送请求和读取响应。如果客户端确实在短时间不需要这个连接再断开。

线程池版本

如果是很多客户端;那么我们就需要反复创建、销毁线程;需要不少开销。三个颜色代表三个版本
在这里插入图片描述

IO多路复用:如果线程池还是不够用;IO多路复用充分的利用等待的时间;去做别的事情。一个线程处理很多个客户端。给这个线程安排个集合;这个集合放一堆连接;这个线程负责监听这个集合;哪里有数据来线程就处理哪个连接(这些连接在严格意义上还是有分先后的)操作系统提供一些原生API(select,poll,epoll),java用NIO这样的类封装这些API。
比如我们团队需要获取唱跳rap这样的技能;你我他咱三就兵分三路每个人学一种;而不是咱三先去唱;然后再一起去跳;最后rap。

横向对比:UDP和TCP编程
UDP:(无连接、不可靠传输、面向数据报、全双公)一个socket可以发也可以收;不用建立连接就能收发数据;面向数据报也是可以体现。只是当前无法体现不可靠传输;待网络原理再详细介绍。

TCP:(有连接;可靠传输;面向字节流;全双公)
TCP在这里也是没有体现出可靠传输的;后面网络原理TCP详细介绍;毕竟这个TCP很大一个意义是为了解决可靠传输问题;在后续网络网络原理之TCP讲详细介绍

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

20计科-廖雨旺

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

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

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

打赏作者

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

抵扣说明:

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

余额充值