网络编程之TCP

hi,大家好,今天为大家带来TCP协议的相关知识

认识TCP的相关方法

实现TCP版本的回显服务器

实现多线程版本的TCP回显服务器

实现线程池版本的TCP回显服务器

认识TCP方法

TCP也有两个核心的类
Socket和SeverSocket
SeverSocket是给服务器用的
Socket的话,客户端可以用,服务器也可以用

先来看看ServerSocket

ServerSocket() 创建未绑定的服务器套接字。
ServerSocket(int port) 创建绑定到指定端口的服务器套接字。

但我们一般在写服务器的时候都会指定服务器端口号
就像饭店,也得有一个固定位置

Socket accept() 侦听要连接到此套接字并接受它。

accept就是接收的意思,客户端向服务器发起连接请求,在内核中进行连接,accept这里是应用程序层面的接受,就是把连接好的连接拿出来让应用程序连起来,这里先简单的认为是连接,后面讲到TCP的三次握手四次招手再具体介绍
ServerSocket是创建TCP服务端的API
下面再来看看Socket

Socket() 创建一个未连接的套接字,并使用系统默认类型的SocketImplort。
Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号。
public Socket(String host,
int port)
throws UnknownHostException,
IOException
创建流套接字并将其连接到指定主机上的指定端口号。

在这里插入图片描述

用于从该套接字读取字节的输入流。
在这里插入图片描述
用于将字节写入此套接字的输出流。
InputStream和OutputStream是字节流,TCP是面对字节流的
InputStream是读数据,也就是从网卡接收
OutputStream是写数据,也就是从网卡发送

方法就认识这么多,下面来写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;



/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 20:43
 */
public class TCPEchoSever {
    //severSocket就是外场拉客的小哥
    //clientSocket就是内场服务的小姐姐
    //severSocket只有一个,clientSocket会给每个客户端都分配一个

    private ServerSocket serverSocket=null;

    public TCPEchoSever(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }
    //通过这个方法处理一个连接
    //读取请求
    //根据请求计算响应
    //把响应返回给客户端

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());//得到IP和端口号
        //try()这种写法,括号中允许有多个流对象,用;来分割
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true){
                //1.读取请求
                if(!scanner.hasNext()){
                    //读取的流到了结尾(对端关闭)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                //直接使用scanner读取一段字符串
                String request=scanner.next();
                //2.根据请求响应
                String response=process(request);
                //3.把响应写回到客户端,不要忘了响应也要戴上换行
                printWriter.println(response);
                System.out.println(response);
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }

    }

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

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

}

我们来运行一下程序
先启动服务器
在这里插入图片描述
在这里插入图片描述
看到客户端也启动了,但是输入却没有响应,这是有问题的
为什么客户端输入消息,但是客户端服务器没有任何响应呢
原因就是我们进行TCP的通信媒介是网卡,网卡读取速度比硬盘还要慢,因此为了提高IO效率,我们一般先把数据放到缓冲区上,然后再手动刷新,再到网卡上
在这里插入图片描述
这里的打印,是从网卡上读取数据打印的,但是打印是无效的,因为此时数据在缓冲区,需要刷新一下,也就是使用printWriter的flush方法
同样在服务器的在这里插入图片描述
打印响应也要进行刷新
上代码
服务器

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;



/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 20:43
 */
public class TCPEchoSever {
    //severSocket就是外场拉客的小哥
    //clientSocket就是内场服务的小姐姐
    //severSocket只有一个,clientSocket会给每个客户端都分配一个

    private ServerSocket serverSocket=null;

    public TCPEchoSever(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }
    //通过这个方法处理一个连接
    //读取请求
    //根据请求计算响应
    //把响应返回给客户端

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());//得到IP和端口号
        //try()这种写法,括号中允许有多个流对象,用;来分割
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true){
                //1.读取请求
                if(!scanner.hasNext()){
                    //读取的流到了结尾(对端关闭)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                //直接使用scanner读取一段字符串
                String request=scanner.next();
                //2.根据请求响应
                String response=process(request);
                //3.把响应写回到客户端,不要忘了响应也要戴上换行
                printWriter.println(response);
                printWriter.flush();
                System.out.println(response);
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }

    }

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

    public static void main(String[] args) throws IOException {
        TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
        tcpEchoSever.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;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 21:16
 */
public class TCPEchoClient {
    private Socket socket=null;

    public TCPEchoClient(String serverIp,int port) throws IOException {
        //这个操作相当于让客户端和服务器建立tcp连接
        //这里的连接连上了,服务器的accept就会返回
        socket=new Socket(serverIp,port);

    }
    public void start(){
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            PrintWriter printWriter=new PrintWriter(outputStream);
            Scanner scannerFromSocket=new Scanner(inputStream);
            while(true){
                //1.从键盘上读取用户输入的内容
                System.out.print("->");
                String request=scanner.next();
                //2.把读取的内容构造成请求,发送给服务器
                //注意,这里的发送,是带有换行的
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                String response=scannerFromSocket.next();
                //4.把响应结果显示到控制台上
                System.out.printf("req: %S; resp: %s\n",request,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();

    }

}

运行结果
在这里插入图片描述
在这里插入图片描述
现在就正常了
我们来总结一下TCP通信过程
1.首先要开启服务器,start这里,然后运行到accept这里,阻塞等待
等待客户端的请求
2.客户端启动时,调用Socket方法,和服务器在内核中建立连接,连接成功以后,服务器的accept就返回了
3.服务器这边进入processConnection方法,尝试从客户端读取请求,但是由于此时用户还没有输入,所以读取操作也会阻塞等待
3.客户端这边往下执行时,从控制台读取用户输入,也会阻塞,因为用户可能不会立即输入
4.当用户输入,客户端发请求出去,客户端代码继续执行,到读取服务器响应时,再次阻塞
5.服务器收到客户端的请求,从next这里返回,执行process方法,执行println,把响应写回给客户端

6.客户端这边收到服务器的响应打印到控制台上,同时进入下一次循环,等待用户输入
6.服务器这边回到循环开始的地方,继续尝试获取客户端的请求,然后阻塞等待

对于缓冲区和网卡之间的切换再来说一说
在发数据的时候,必须通过网卡发送,应用程序层面无法直接通过网卡发送数据
接收数据的时候,应用软件无法直接通过网卡接收
那么怎么办呢,在tcp通信中,是存在缓冲区这样的概念的,我们通过缓冲区进行发送和接收
下面来画个图
我们先来画跨主机通信的,那么就需要两个网卡
在这里插入图片描述

传输的数据是需要套接socket的,可以把socket当做一个文件,套接tcp数据报进行传输,就像水是在水管里运输的,水管就是相当于socket,水相当于要传输的数据
send:
客户端发出的数据,要先通过socket进行包装,通过socket对象的getOutputStream方法获取与这个socket相关的outputStream对象,使用write()方法写入到缓冲区,然后将发送缓冲区的数据送到网卡,网卡将数据发到服务器端的接收缓冲区,再发给服务器
receive:
服务器在收到数据的时候,处理完数据,就又通过socket,发送到服务器这边的发送缓冲区,发送缓冲区发给网卡,然后交给客户端这边,客户端通过socket对象的getInputStream方法获取与这个socket相关的inputStream对象,客户端就可以通过read()方法从接收缓冲区读取数据
下图是在同一个主机通信的,就使用一个环回网卡就行!!!

在这里插入图片描述
发送数据和接收数据过程是一样,就不赘述了
现在这个程序是没有问题了,但是在互联网的世界中,我们知道肯定是有很多客户端要访问服务器
那么这个代码这样写是不支持多个客户端同时访问的,我们来看看
在这里插入图片描述
在idea勾选,就可以在idea同时运行很多个客户端
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到,我们在客户端1输入请求,是有响应的,在其他的客户端上输入请求,是没有响应的
这里其实是一个bug.是代码的bug
我们来分析一下
在这里插入图片描述
在这里插入图片描述

当客户端1来的时候,操作系统从内核拿出连接,然后接受连接,调用processConnectino方法,进入processConnecion方法那么只有当客户端1下线其他客户端才能相继连接,我们想要循环处理客户端1的数据,还要循环accept,接受其他的客户端请求,那么我们要咋样解决这个问题呢?
用多线程
在这里插入图片描述
这样写就可以实现一边循环处理原来客户端的请求,还能循环接收其他的客户端进行accept
注意:这里的线程是一个一个创建的,就是串行创建,而在执行的时候是并发的
咋样通俗理解呢
拿售楼来说,一个小哥拉来客户1号,交给一个小姐姐1,进行服务,在小姐姐1服务客户1的时候,这个小哥又拉来一个客户2,再来一个小姐姐2,服务,依次类推,这些客户之间,小姐姐之间是互不影响的,是一起执行的,就是这样理解的了

我们把完整代码给大家
客户端

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;



/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 20:43
 */
public class TCPEchoSever {
    //severSocket就是外场拉客的小哥
    //clientSocket就是内场服务的小姐姐
    //severSocket只有一个,clientSocket会给每个客户端都分配一个

    private ServerSocket serverSocket=null;

    public TCPEchoSever(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){
            Socket clientSocket=serverSocket.accept();
            Thread thread=new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });


        }
    }
    //通过这个方法处理一个连接
    //读取请求
    //根据请求计算响应
    //把响应返回给客户端

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());//得到IP和端口号
        //try()这种写法,括号中允许有多个流对象,用;来分割
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true){
                //1.读取请求
                if(!scanner.hasNext()){
                    //读取的流到了结尾(对端关闭)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                //直接使用scanner读取一段字符串
                String request=scanner.next();
                //2.根据请求响应
                String response=process(request);
                //3.把响应写回到客户端,不要忘了响应也要戴上换行
                printWriter.println(response);
                printWriter.flush();
                System.out.println(response);
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }

    }

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

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

}

服务器

import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 21:16
 */
public class TCPEchoClient {
    private Socket socket=null;

    public TCPEchoClient(String serverIp,int port) throws IOException {
        //这个操作相当于让客户端和服务器建立tcp连接
        //这里的连接连上了,服务器的accept就会返回
        socket=new Socket(serverIp,port);

    }
    public void start(){
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            PrintWriter printWriter=new PrintWriter(outputStream);
            Scanner scannerFromSocket=new Scanner(inputStream);
            while(true){
                //1.从键盘上读取用户输入的内容
                System.out.print("->");
                String request=scanner.next();
                //2.把读取的内容构造成请求,发送给服务器
                //注意,这里的发送,是带有换行的
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                String response=scannerFromSocket.next();
                //4.把响应结果显示到控制台上
                System.out.printf("req: %S; resp: %s\n",request,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();

    }

}

线程频繁的创建和销毁是耗费资源的,我们升级代码,采用线程池,避免线程销毁,用完放到线程池里面,更加高效
在这里插入图片描述
看看完整代码
客户端

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;


/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 20:43
 */
public class TCPEchoSever {
    //severSocket就是外场拉客的小哥
    //clientSocket就是内场服务的小姐姐
    //severSocket只有一个,clientSocket会给每个客户端都分配一个

    private ServerSocket serverSocket=null;

    public TCPEchoSever(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }

    public void start() throws IOException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        System.out.println("服务器启动!");
        while(true){

            Socket clientSocket=serverSocket.accept();

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



        }
    }
    //通过这个方法处理一个连接
    //读取请求
    //根据请求计算响应
    //把响应返回给客户端

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
                clientSocket.getPort());//得到IP和端口号
        //try()这种写法,括号中允许有多个流对象,用;来分割
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()) {
            Scanner scanner=new Scanner(inputStream);
            PrintWriter printWriter=new PrintWriter(outputStream);
            while(true){
                //1.读取请求
                if(!scanner.hasNext()){
                    //读取的流到了结尾(对端关闭)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                //直接使用scanner读取一段字符串
                String request=scanner.next();
                //2.根据请求响应
                String response=process(request);
                //3.把响应写回到客户端,不要忘了响应也要戴上换行
                printWriter.println(response);
                printWriter.flush();
                System.out.println(response);
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }

    }

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

    public static void main(String[] args) throws IOException {
        TCPEchoSever tcpEchoSever=new TCPEchoSever(9090);
        tcpEchoSever.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;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: WHY
 * Date: 2023-04-07
 * Time: 21:16
 */
public class TCPEchoClient {
    private Socket socket=null;

    public TCPEchoClient(String serverIp,int port) throws IOException {
        //这个操作相当于让客户端和服务器建立tcp连接
        //这里的连接连上了,服务器的accept就会返回
        socket=new Socket(serverIp,port);

    }
    public void start(){
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()){
            PrintWriter printWriter=new PrintWriter(outputStream);
            Scanner scannerFromSocket=new Scanner(inputStream);
            while(true){
                //1.从键盘上读取用户输入的内容
                System.out.print("->");
                String request=scanner.next();
                //2.把读取的内容构造成请求,发送给服务器
                //注意,这里的发送,是带有换行的
                printWriter.println(request);
                printWriter.flush();
                //3.从服务器读取响应
                String response=scannerFromSocket.next();
                //4.把响应结果显示到控制台上
                System.out.printf("req: %S; resp: %s\n",request,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();

    }

}

补充:

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以 多次收发数据

在这里插入图片描述
这就是这期的所有内容了,我们下期再见,886!🌸🌸🌸

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值