【网络编程】TCP

这篇具有很好参考价值的文章主要介绍了【网络编程】TCP。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

【网络编程】TCP

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇

🔮一. TCP流套接字编程

  • ServerSocket API

ServerSocket 是创建TCP服务端Socket的API。

ServerSocket:是服务器端使用的 Socket

ServerSocket 构造方法:

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法:

方法签名 方法说明
Socket accept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待
void close() 关闭此套接字

accept:没有参数,返回值是一个 Socket 对象,功能是等待有客户端和服务器建立上连接,accept 则会把这个连接获取到进程中,进一步的通过返回值的 Socket 对象来和客户端进行交互。

在此处可以举例为售楼,比如外场有男生拉客人,女生内场介绍售楼服务,其中 ServerSocket 就是外场连接,通过 accept 把连接交给了 Socket ,然后 Socket 对象和客户端进行沟通。

  • Socket API

Socket:服务器和客户端都会使用的 Socket 。

Socket 是客户端 Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket 构造方法:

方法签名 方法说明
Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket 方法:

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

第二和第三个方法的传输数据,不是直接通过 Socket 对象,而是 Socket 内部包含了输入流对象(接收)输出流对象(发送)


💿二. TCP中的长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

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

对比以上长短连接,两者区别如下:

  1. 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  2. 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  3. 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

📀三. 写一个 TCP 版本的 回显服务器-客户端

服务器启动的时候,进行 accept ,accept 进行的工作是拉客,对于操作系统来说,建立 TCP 连接,是内核工作,accept 要干的就是等连接建立好了之后把这个连接拿到应用程序中,如果当前连接还没建立,accept 就会阻塞!accept 相当于,有人给你打电话了,你按下接听键!如果没人打电话,阻塞等待有人给你真的打电话了才行。

如果服务器返回多次响应,其实这多次响应,都是以字节为单位返回给客户端的,客户端操作系统内核就会收到这些字节,然后调用 read / scanner.next 这样的方法的时候,就是从内核中读取。

  • read :如果使用字节流的 read ,就是在读取固定长度的字节。即使返回 10 次,每次返回 10 个字节,这里 read (buf[100]),这种操作一次性就读出来了,也可以多次读取
  • scanner.next :隐含的东西,这里的 next 是读到空白符(空格,回车,换行,制表,翻页,垂直制表…)

测试网络程序的时候,务必要先启动服务器,再启动客户端。

客户端代码:

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() throws IOException {
        //new 这个对象,是要和服务器建立连接的
        //建立连接,是要知道服务器在哪里
        socket = new Socket("127.0.0.1",8000);
    }

    public void start() throws IOException {
        //由于我们建立的是长连接,一个连接会处理 N 个请求和响应
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //从控制台读取用户输入
                System.out.println("> ");
                String request = scanner.next();
                //2. 把请求发送给服务器
                printWriter.write(request);
                printWriter.flush();
                //3. 从服务器读取响应
                String response = scannerNet.next();
                //4. 把结果显示到界面上
                System.out.printf("req : %s; resp : %s\n",request,response);
            }
        }
    }

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

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("服务器启动!");
        while (true){
            //如果当前没有客户端来进行连接,就会阻塞等待
            Socket clientSocket = serverSocket.accept();
            processConnect(clientSocket);
        }
    }

    //通过这个方法,给当前连上的这个客户端,提供服务
    //一个连接过来了,服务方式可能有两种
    //1. 一个连接只进行一次数据交互(一个请求 + 一个响应)  短连接
    //2. 一个连接进行多次数据交互(N 个请求 + N 个响应)   长连接
    //此处来写长连接版本
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            //这里是长连接的写法,需要用循环来获取到多次交互的情况
            while (true){
                if(!scanner.hasNext()){
                    //断开连接,当客户端断开连接的时候,此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //1. 读取请求并解析
                String request = scanner.next();
                //2. 根据请求计算响应
                String response = process(request);
                //3. 把响应返回给客户端
                printWriter.write(response);
                // 刷新一下缓冲区,避免数据没有发出去
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),
                        request,response);
            }
        }
    }
    public String process(String req){
        return req;
    }

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

运行效果:

【网络编程】TCP
【网络编程】TCP

但是在此处我们发现客户端和服务器并没有想我们之前那样连接起来,输入内容之后,没发生反应,有两种可能性:

  1. 客户端没发出去(很可能没加 flush)
  2. 服务器收到了没有处理(很可能服务器还在阻塞的状态)

经过排查,猜测上面代码问题应该出现在服务器中,但是我们如何确认阻塞在哪里了呢?

我之前写过的,线程出现问题,可以查看 jconsole查看当前线程状况,看到每个线程的调用栈

客户端中:

【网络编程】TCP

服务器中:

【网络编程】TCP

发现都是在 next 中阻塞了,下面两行就是代码阻塞的位置。

但是客户端阻塞在这里,是很合理的!等待服务器返回响应,服务器当前确实没有返回响应(如果服务器返回响应了,服务器会打印的,实际上服务器没打印这个日志)

next 啥时候会执行结束?就是当读到空白符空格,回车,换行,制表,翻页,垂直制表…)的时候才结束。

scanner.next 的行为是:尝试往后读,读到空白符就结束,会返回一个字符串,返回值里是不包含刚才最后的空白符的。由此可见,我们在客户端发送的信息中再加个空白符就行了。

此处会有人说可以用 nextline,nextline 是判定 /n,next 是判断空白符(包含 /n),都差不多。

所以此处最好的修改就是把 next 后面含有 write 的写法改成 println

客户端中第二步:把请求发送给服务器改为:printWriter.println(request);
服务器中第三步:把响应返回给客户端改为:printWriter.println(response);

改变后的代码执行就正确了

【网络编程】TCP
【网络编程】TCP

但是但是但是!!!bug 并没有全部解决,还有两个 bug。

  1. 上述代码中,使用 clientSocket !用完之后,是否要关闭呢?当然要关闭!和上面的 ServerSocket 不同!

生命周期不同,ServerSocket 的生命周期,是跟随整个程序的,clientSocket 的生命周期,只是当前连接!就应该在连接之后,把这里的 socket 进行关闭。如果不关闭,妥妥的资源泄露,ServerSocket 只有一个,clientSocket 会有无数个!每个客户端的连接,都是一个!!!

因此解决方案就是在 try 后面加个 finally ,finally 中写 clientSocket.close(); 即可

  1. 上述代码无法处理多个客户端

当第一个客户端连上服务器之后,服务器提示 “建立连接”
当第二个客户端也连接上的时候,服务器没提示
 
第一个客户端发的请求,可以正常处理
第二个客户端发的请求,不能处理
 
当把第一个客户端关闭之后,第二个客户端才能真正连接成功,之前发的请求,才有回应。

在服务器代码中:

【网络编程】TCP
【网络编程】TCP

当第一个客户端连接上之后,服务器就进入了 processConnect 代码,就在 while 循环 next 这里阻塞了,processConnect 方法就执行不完了,同时也就无法第二次执行到 accept ,也就无法处理第二个客户端。

next 和 循环 影响了服务器第二次调用 accept

因此解决方案就是既能够执行到 processConnect 里面的循环,又能够执行到 accept 即可,所以在这里,我们可以使用多线程。在主线程中执行 accept ,创建一个新的线程来调用 processConnect ,这样原来主线程就不会受影响。

【网络编程】TCP

执行效果:

  • 客户端1
    【网络编程】TCP

  • 客户端2
    【网络编程】TCP

  • 客户端3
    【网络编程】TCP

  • 服务器
    【网络编程】TCP

虽然比最开始单线程有提升,但是涉及到频繁创建销毁线程,在高并发的状态下,负担比较重,因此这里可以再优化使用线程池。

使用线程池,来解决频繁创建销毁线程的问题

//注意线程池在循环外面创建
ExecutorService service = Executors.newCachedThreadPool();
//循环内部
service.submit(new Runnable() {
    @Override
    public void run() {
        try {
            processConnect(clientSocket);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

当前是使用了线程池解决了问题,但是如果并发量太高了!就会导致池子里的线程特别特别多!就说明内存等资源调度开销特别大,,于是乎有了进一步的改进,是否能减少线程的数目?当前是一个线程对应一个客户端,能不能让一个线程对应多个客户端?当然可以,但是此处就不再拓展了,想知道的老铁可以去搜搜 “IO 多路复用”,本质上就是一个线程处理多个 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;

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){
            //如果当前没有客户端来进行连接,就会阻塞等待
            Socket clientSocket = serverSocket.accept();
            //不再直接调用了,创建一个新的线程,让线程来调用他
            //版本1: 使用单线程,存在 bug ,无法处理多线程情况
            //processConnect(clientSocket);

			//版本2: 使用多线程,主线程负责拉客,新线程负责通信
			//虽然比版本 1 有提升,但是涉及到频繁创建销毁线程,在高并发的状态下,负担比较重
            //Thread t = new Thread(()->{
            //    try {
            //        processConnect(clientSocket);
            //    } catch (IOException e) {
            //        e.printStackTrace();
            //   }
            //});
            //t.start();

			//版本3: 使用线程池,来解决频繁创建销毁线程的问题
			//此处不太适合使用 "固定个数的"
			//在外面创建线程池
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnect(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    //通过这个方法,给当前连上的这个客户端,提供服务
    //一个连接过来了,服务方式可能有两种
    //1. 一个连接只进行一次数据交互(一个请求 + 一个响应)  短连接
    //2. 一个连接进行多次数据交互(N 个请求 + N 个响应)   长连接
    //此处来写长连接版本
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            //这里是长连接的写法,需要用循环来获取到多次交互的情况
            while (true){
                if(!scanner.hasNext()){
                    //断开连接,当客户端断开连接的时候,此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //1. 读取请求并解析
                String request = scanner.next();
                //2. 根据请求计算响应
                String response = process(request);
                //3. 把响应返回给客户端
                printWriter.println(response);
                // 刷新一下缓冲区,避免数据没有发出去
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),
                        request,response);
            }
        }finally {
            //加在这里是更稳妥的做法
            clientSocket.close();
        }
    }
    public String process(String req){
        return req;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        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() throws IOException {
        //new 这个对象,是要和服务器建立连接的
        //建立连接,是要知道服务器在哪里
        socket = new Socket("127.0.0.1",8000);
    }

    public void start() throws IOException {
        //由于我们建立的是长连接,一个连接会处理 N 个请求和响应
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //从控制台读取用户输入
                System.out.println("> ");
                String request = scanner.next();
                //2. 把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();
                //3. 从服务器读取响应
                String response = scannerNet.next();
                //4. 把结果显示到界面上
                System.out.printf("req : %s; resp : %s\n",request,response);
            }
        }
    }

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

TCP 版本的字典查找

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

public class TcpDictServer extends TcpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("fuck","卧槽");
    }

    public String process(String req){
        return dict.getOrDefault(req,"俺也不知道是啥");
    }

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

运行结果展示:

【网络编程】TCP
【网络编程】TCP

【网络编程】TCP文章来源地址https://www.toymoban.com/news/detail-415779.html

到了这里,关于【网络编程】TCP的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 网络编程 TCP电子网络词库

     电子词典: 要求: 登录注册功能,不能重复登录,重复注册。用户信息也存储在数据库中。 单词查询功能 历史记录功能,存储单词,意思,以及查询时间,存储在数据库 基于TCP,支持多客户端连接 采用数据库保存用户信息与历史记录 将dict.txt的数据导入到数据库中保存。

    2024年02月17日
    浏览(48)
  • 网络编程 -- 简易TCP网络程序

      字符串回响程序类似于 echo 指令,客户端向服务器发送消息,服务器在收到消息后会将消息发送给客户端,该程序实现起来比较简单,同时能很好的体现 socket 套接字编程的流程。      这个程序我们已经基于 UDP 协议实现过了,换成 TCP 协议实现时,程序的结构是没有变化

    2024年04月25日
    浏览(45)
  • 网络编程『简易TCP网络程序』

    🔭个人主页: 北 海 🛜所属专栏: Linux学习之旅、神奇的网络世界 💻操作环境: CentOS 7.6 阿里云远程服务器 随着数字时代的来临,TCP网络程序已成为程序员不可或缺的技术领域。本博客将带领读者深入研究,从最基础的字符串回响开始,逐步探索至多进程、多线程服务器

    2024年02月04日
    浏览(50)
  • 网络编程【TCP流套接字编程】

    目录 TCP流套接字编程 1.ServerSocket API 2.Socket API 3.TCP中的长短连接 4.回显程序(短连接) 5.服务器和客户端它们的交互过程 6.运行结果及修改代码   ❗❗两个核心: ServerSocket     Socket 1.ServerSocket API ✨ ServerSocket 是创建 TCP服务端Socket的API ServerSocket 构造方法: ServerSocket 方法 :

    2023年04月12日
    浏览(149)
  • 网络编程 —— TCP 和 UDP 编程详解

    目录 网络编程主要函数介绍 1. socket 函数 2. bind 函数 3. listen 函数 4. accept 函数 5. connect 函数 6. send 函数 7. recv 函数 8. recvfrom 函数 9. sendto 函数 TCP 和 UDP 原理上的区别 TCP 编程 服务端代码: 客户端代码: UDP 编程 服务端代码: 客户端代码: 1. socket 函数 int socket(int domain, int

    2024年02月04日
    浏览(44)
  • 【网络编程】网络编程概念,socket套接字,基于UDP和TCP的网络编程

    前言: 大家好,我是 良辰丫 ,今天我们一起来学习网络编程,网络编程的基本概念,认识套接字,UDP与TCP编程.💞💞💞 🧑个人主页:良辰针不戳 📖所属专栏:javaEE初阶 🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。 💦期待大家三连,关注

    2023年04月20日
    浏览(61)
  • 网络编程 tcp udp http编程流程 网络基础知识

    OSI分层:应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 tcp/ip: 应用层 传输层 网络层 数据链路 ip地址:唯一标识一台主机 ipv4 32位 ipv6 128位 寻址 可以反映物理上的一个变化 MAC地址:48 固化在计算机中 ip地址又两部分构成:网络号+主机号 端口号:标识一个应用程序

    2024年02月13日
    浏览(76)
  • TCP/IP网络编程(一) 理解网络编程和套接字

    网络编程和套接字概要 网络编程就是编写程序使两台联网的计算机相互交换数据 为了与远程计算机进行数据传输,需要连接因特网,而编程种的套接字就是用来连接该网络的工具。 构建套接字 1.调用soecket函数创建套接字 2.调用bind函数给套接字分配地址 3.调用listen函数将套

    2024年02月11日
    浏览(175)
  • 【Linux网络编程】网络编程套接字(TCP服务器)

    作者:爱写代码的刚子 时间:2024.4.4 前言:本篇博客主要介绍TCP及其服务器编码 只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP地址 但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换 字符串转in

    2024年04月14日
    浏览(79)
  • TCP网络编程

    上期分享了UDP下的网络编程,相信大家对网络通信已经有了自己的一些认知,那么我们本期就进一步来看一下TCP下的网络编程,感受一下TCP和UDP的相同点和不同点,实践证明一切,让我们来看看吧! (1)发送端发送hello world给接收端 (2)接收端进行接收并打印收到的消息

    2024年02月07日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包