网络编程基础之TCP编程

网络编程是开发中经常需要用到的一个点,也是面试中必考题,本节对网络编程中常用知识点进行简要概述。

原文链接网络编程基础(上)

网络编程基础之UDP编程请看UDP

以下知识点均来源于廖雪峰官方网站https://www.liaoxuefeng.com/wiki/1252599548343744/1323711850348577

网络模型

计算机网络

计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。

那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。

网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect) 网络模型是ISO组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

TCP/IP 模型

互联网实际使用的TCP/IP模型并不是对应到OSI的7层模型,而是大致对应OSI的5层模型:

OSITCP/IP
应用层应用层
表示层
会话层
传输层传输层
网络层IP层
链路层网络接口
物理层物理

小结

  • 计算机网络:由两台或更多计算机组成的网络;
  • 互联网:连接网络的网络;
  • IP地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
  • 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
  • 网络协议:互联网使用TCP/IP协议,它泛指互联网协议簇;
  • IP协议:一种分组交换传输协议;
  • TCP协议:一种面向连接,可靠传输的协议;
  • UDP协议:一种无连接,不可靠传输的协议。

TCP 编程

Socket

在开发网络应用程序的时候,我们又会遇到Socket这个概念。Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络:

Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。

为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。

一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开。

使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。

因此,当Socket连接成功地在服务器端和客户端之间建立后:

  • 对服务器端来说,它的Socket是指定的IP地址和指定的端口号;
  • 对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。

服务端实现

要使用Socket编程,我们首先要编写服务器端程序。Java标准库提供了ServerSocket来实现对指定IP和指定端口的监听。ServerSocket的典型实现代码如下:

/**
 * @Auther Mario
 * @Date 2020-12-30 12:38
 * @Version 1.0
 * Socket 通信编程之TCP
 */
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666);
        System.out.println("服务端口正在监听");
        for(;;){
            //处理每个新的请求
            Socket sock = ss.accept();
            System.out.println("已经接受客服端请求");
            //每个请求分配一个线程处理
            Thread t = new Handle(sock);
            t.start();
        }
    }
}
class Handle extends Thread{
    private Socket socket;
    public Handle(Socket socket) {
        this.socket = socket;
    }
    public void run(){
        try(InputStream inputStream = this.socket.getInputStream()){
            try(OutputStream outputStream = this.socket.getOutputStream()){
                handle(inputStream,outputStream);

            }
        }catch (Exception e){
            try {
                this.socket.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            System.out.println("client disconnected");
        }

    }
    
    private void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        bufferedWriter.write("hello\n");
        bufferedWriter.flush();
        for(;;){
            //读取请求的每一行数据,直到关键字'bye',发送数据
            String read = bufferedReader.readLine();
            if(read.equals("bye\n")){
                bufferedWriter.write("bye\n");
                bufferedWriter.flush();
                break;
            }

            bufferedWriter.write("ok:  " + read + "bye\n");
            bufferedWriter.flush();
        }
    }
}

服务器端通过代码:

ServerSocket ss = new ServerSocket(6666);

在指定端口「6666」监听。这里我们没有指定IP地址,表示在计算机的所有网络接口上进行监听。

如果ServerSocket监听成功,我们就使用一个无限循环来处理客户端的连接:

for (;;) {
    Socket sock = ss.accept();
    Thread t = new Handler(sock);
    t.start();
}

注意到代码ss.accept()表示每当有新的客户端连接进来后,就返回一个Socket实例,这个Socket实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。

我们在多线程编程的章节中介绍过线程池,这里也完全可以利用线程池来处理客户端连接,能大大提高运行效率。

如果没有客户端连接进来,accept()方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()就可以获取新的连接。

客服端实现

/**
 * @Auther mashang
 * @Date 2020-12-30 14:04
 * @Version 1.0
 * Socket 通信编程 TCP
 */
public class Client {

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("localhost",6666);
        // 用于读取网络数据:
        try(InputStream inputStream = socket.getInputStream()){
            // 用于写入网络数据:
            try(OutputStream outputStream = socket.getOutputStream()){
                handle(inputStream,outputStream);
            }
        }
        socket.close();
        System.out.println("disconnected");
    }
    
    private static void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server]: " + bufferedReader.readLine());
        for(;;){
            System.out.println(">>>");
            String s = scanner.nextLine();
            bufferedWriter.write(s);
            bufferedWriter.newLine();
            bufferedWriter.flush();
            String resp = bufferedReader.readLine();
            System.out.println("<<<" + resp);
            if(resp.equals("bye")){
                break;
            }
        }
    }
}

客户端程序通过:

Socket sock = new Socket("localhost", 6666);

连接到服务器端,注意上述代码的服务器地址是**“localhost”** ,表示本机地址,端口号是「6666」。如果连接成功,将返回一个Socket实例,用于后续通信。

自我总结:通过以上对服务端和客服端简要实现得知,服务端在6666端口一直处于监听状态,等待客服端连接,收到客户端请求后新建一个Handle线程处理请求,该线程主要处理服务端通过socket流进行网络通信,inputStream从客户端接收的输入流,outputStream发给客户端的输出流,handle()方法处理输入输出流,在通信结束后关闭socket连接。handle()把inputStream和outputStream分别包装为bufferedReader和bufferedWriter,建立连接后服务端首先发送"hello"给客户端bufferedWriter.write(“hello\n”),flush()用于强制把缓冲区数据发送出去,然后一直等待客户端发送数据,进而读取数据,回复信息。客户端则连接服务端端口建立通信,通过socket传递数据,从输入台输入数据,flush()发送数据,通过bufferedReader.readLine()读取数据,直到“bye”完成通信,断开连接。

Socket流

当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream和OutputStream来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:

// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();

最后我们重点来看看,为什么写入网络数据时,要调用flush()方法。

如果不调用flush(),我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()强制把缓冲区数据发送出去。

小结

使用Java进行TCP编程时,需要使用Socket模型:

  • 服务器端用ServerSocket监听指定端口;
  • 客户端使用Socket(InetAddress, port)连接服务器;
  • 服务器端用accept()接收连接并返回Socket;
  • 双方通过Socket打开InputStream/OutputStream读写数据;
  • 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
  • flush()用于强制输出缓冲区到网络。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值