草庐IT

网络编程套接字之三【TCP】

快到锅里来呀 2023-11-20 原文

目录

1. ServerSocket API(给服务器端使用的类)

2. Socket API(既给服务器使用,也给客户端使用)

3. 写TCP回显—服务器

4. 使用线程池后的TCP服务器代码(最终)

5. 写回显-客户端

6. TCP回显—客户端代码

7. 运行回显服务器和客户端


TCP流套接字编程

1. ServerSocket API(给服务器端使用的类)

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

 构造方法

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

方法

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

2. Socket API(既给服务器使用,也给客户端使用)

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

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

构造方法

方法签名说明
Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的

进程建立连接(尝试和指定的服务器建立连接)

方法

方法签名说明
InetAddress getInetAddress()返回套接字所连接的地址(返回套接字获取到对方的IP地址和端口)
InputStream getInputStream()返回此套接字的输入流(通过Socket可以获取到两个流对象,分别用来读和写)
OutputStream getOutputStream()返回此套接字的输出流

3. 写TCP回显—服务器

1.先写一个ServerSocket对象

    private ServerSocket listenSocket = null;

2.下面写Tcp服务器构造方法

    public TcpEchoServer(int port) throws IOException {
     listenSocket = new ServerSocket(port);
    }

3.写一个启动服务器的方法start(),在start中写上while循环来执行

   a)调用accept来接收客户端的连接

   b)再处理这个连接,这里写一个processConnection()方法来处理

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            //1. 先调用 accept 来接受客户端的连接
            Socket clientSocket = listenSocket.accept();
            //2. 再处理这个连接
            processConnection(clientSocket);
        }
   }

4.下面来写这个processConnection()方法,处理连接客户端连接

   方法中写try(这里写上InputStream(读)和OutPutStream(写)对象,写在try中帮助资源回收) {这里写上写具体处理逻辑步骤}

注意最后必须要写上finally来close关闭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) {
           
            }
        }finally {
            //这里要关闭socket,是因为
            //socket也是一个文件,一个进程能够同时打开的文件个数有上限(PCB文件描述符表,不是无限的
            clientSocket.close();
        }
    }

   a)读取请求并解析

                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()) {
                    //读完了,连接可以断开了
                    System.out.printf("[%s:%d] 客户端下线!\n",
                            clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                String request = scanner.next();

   b)根据请求计算响应(这里写一个process方法)

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

 c)响应写回到客户端

                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //刷新缓冲区确保数据确实通过网卡发送出去了
                printWriter.flush();

   d)将发送的信息显示到服务器界面上

                System.out.printf("[%s:%d] req: %s; resp: %s\n",
                        clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),
                        request,response);

5.最后再写上mian方法来执行服务器

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

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 28463
 * Date: 2022—10—14
 * Time: 17:05
 */
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 来接受客户端的连接
            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 {
            clientSocket.close();
        }
    }

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

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

 下面思考为啥代码最后finally要执行clientSocket的close,而前面的listenSocket以及UDP程序中的socekt为啥就没close?

但是上面的代码有个问题,只能处理一个客户端的请求,(本质上第二个客户端的消息是发出去了,但服务器此时还在执行第一个客户端的请求,只要从第一个客户端这里出来,服务器就会立刻执行第二个客户端的消息)

我们希望的是既能够快速重复的调用到accept(也就是连接多个客户端),又能够循环的处理客户端的请求。所以就需要使用到多线程了

那么为什么前面UDP就不需要考虑这个问题,而TCP需要考虑 

UDP是无连接,客户端直接发消息就行(不必专注于处理某一个客户端)

TCP建立连接之后,要处理客户端的多次请求,才导致无法快速的调用到accept(长连接)(主要原因)

如果TCP每个连接只处理一个客户端的请求,也能够保证快速调用到accept(短连接)

 下面使用多线程,给每个客户端连上来的都分配一个新的线程负责处理请求

 但是直接这样使用多线程,如果循环多次,对应就会创建很多线程,等线程执行完,又会消毁很多的线程,所以更好的方法就是使用线程池

4. 使用线程池后的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.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 28463
 * Date: 2022—10—14
 * Time: 17:05
 */
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("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true) {
            //1. 先调用 accept 来接受客户端的连接
            Socket clientSocket = listenSocket.accept();
            //2. 再处理这个连接,这里应该要使用多线程,每个客户端连上来都分配一个新的线程负责处理
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    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,是因为
            //socket也是一个文件,一个进程能够同时打开的文件个数有上限(PCB文件描述符表,不是无限的
            clientSocket.close();
        }
    }

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

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

5. 写回显-客户端

 1. 先写一个Socket对象,客户端用Socket来建立连接

private Socket socket = null;

2.下面写Tcp客户端构造(和Udp区别较大,有连接和无连接的区别)

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

3.写客户端执行方法start(),给start里面放try,try中执行的就是while,来让客户端循环输入,还要在try后面括号中写上InputStream(读)和OutputStream(写)的对象(写在括号中中try会自动帮助,关掉资源)

    public void start() throws IOException {
        Scanner scan = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while(true) {

            }
         }
    }

while中写  a)从控制台读取数据,构造一个请求

                System.out.println("-> ");
                String request = scan.next();

                 b)发送请求给服务器(PrintWriter)

                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //这个 flush 不要忘记,否则可能导致请求没有真发出去
                printWriter.flush();

                 c)从服务器读取响应

                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();

                 d)把响应显示到界面上

                System.out.println(response);

4.最后再写上mian方法来执行客户端

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

 Udp和Tcp构造的区别,有连接和无连接 

6. TCP回显—客户端代码

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: 28463
 * Date: 2022—10—14
 * Time: 17:06
 */
public class TcpEchoClient {
    //客户端需要使用这个 Socket 对象来建立连接
    private Socket socket = null;

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

    public void start() throws IOException {
        Scanner scan = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while(true) {
                //1.从控制台读取数据,构造成一个请求
                System.out.println("-> ");
                String request = scan.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);
            }
        }
    }

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

7. 运行回显服务器和客户端

 

 

有关网络编程套接字之三【TCP】的更多相关文章

  1. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  2. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  3. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  4. ruby - 是否可以在不实际发送或读取数据的情况下查明 ruby​​ 套接字是否处于 ESTABLISHED 或 CLOSE_WAIT 状态? - 2

    s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成

  5. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  6. ruby-on-rails - Ruby 的 'open_uri' 是否在读取或失败后可靠地关闭套接字? - 2

    一段时间以来,我一直在使用open_uri下拉ftp路径作为数据源,但突然发现我几乎连续不断地收到“530抱歉,允许的最大客户端数(95)已经连接。”我不确定我的代码是否有问题,或者是否是其他人在访问服务器,不幸的是,我无法真正确定谁有问题。本质上,我正在读取FTPURI:defself.read_uri(uri)beginuri=open(uri).readuri=="Error"?nil:urirescueOpenURI::HTTPErrornilendend我猜我需要在这里添加一些额外的错误处理代码...我想确保我采取一切预防措施来关闭所有连接,这样我的连接就不是问题所在,但是我

  7. ruby - Faye WebSocket,关闭处理程序被触发后重新连接到套接字 - 2

    我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d

  8. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  9. Ruby 元编程问题 - 2

    我正在查看Ruby日志记录库Logging.logger方法并从sourceatgithub提出问题与这段代码有关:logger=::Logging::Logger.new(name)logger.add_appendersappenderlogger.additive=falseclass我知道类 最佳答案 这实际上删除了方法(当它实际被执行时)。这是确保close不会被调用两次的保障措施。看起来好像有嵌套的“class 关于Ruby元编程问题,我们在StackOverflow上找到一

  10. ruby - Paperclip:以编程方式分配图像并设置其名称 - 2

    使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案

随机推荐