基于 Socket 网络编程

简介: 基于 Socket 网络编程

前言

我们再进行网络编程时,主要是编写“应用层”代码,如果真正要发送这个数据,需要上层协议调用下层协议,也就是应用层调用传输层,传输层给应用层提供一组 API,统称为 Socket API

一、基于Socket的网络通信传输(传输层)

Socket 套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本操作单元。基于Socket 套接字的网络程序开发就是网络编程。

在这一块,我们对于传输层协议,主要学习两种 Socket 套接字:

数据报套接字:使用传输层UDP协议。UDP,即 User Datagram Protocol(用户数据报协议),传输层协议。

以下为UDP的特点(细节后续介绍):

  1. 无连接:使用 udp 通信的双方,不需要刻意保存对端的相关信息。
  2. 不可靠传输:不关注结果
  3. 面向数据报:以一个 udp 数据报为基本单位
  4. 全双工:双向通信(有接收缓冲区,无发送缓冲区)
  5. 大小受限:一次最多传输 64k

流套接字:使用传输层TCP协议。TCP,即 Transmission Control Protocol(传输控制协议),传输层协议。

以下为TCP的特点(细节后续介绍):

  1. 有连接:使用 TCP 通信双方,则需要刻意各自记录了对方的信息
  2. 可靠传输:发送后尽可能的传输过去,失败了也知道
  3. 面向字节流:以字节流为传输的基本单位,读写方式非常灵活
  4. 全双工:双向通信(有接收缓冲区,也有发送缓冲区)
  5. 大小不限

二、UDP 的数据报套接字编程

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。

这里的 Socket 我们可以类比于 File 对象理解,我们知道如果我们是不能直接操作硬盘的,如果想要操作硬盘就需要借助 File。对象间接操作。Socket 也是类似,它对应于网卡这个硬件设备,如果我们想要操作网卡,就需要一个 Socket 对象间接操作网卡。向 socket 对象中1写数据,就相当于通过网卡发送消息。从 socket 对象中读数据,就相当于通过网卡接收消息。

1、UDP 套接字编程 API

(1)DatagramSocket

DatagramSocket 构造方法

方法签名 方法说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)


说明:对于服务器,一般要手动指定一个固定的端口,客户端则不要求。类似于我去食堂吃饭,给我提供食物的窗口就是一个服务器,而我是接收食物的消费者,可看做是客户端,窗口需要有一个固定的窗口号,便于我找到它,而我在享受食物时,没有固定的座位,哪里有空位就做哪里。

DatagramSocket 方法

方法签名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字

(2)DatagramPacket

DatagramPacket 是 UDP Socket 发送和接收的数据报

DatagramPacket构造方法

方法签名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length) 以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收从offset到指定长度length

DatagramPacket(byte[] buf, int length, InetAddress address, int port)

用来发送数据报,发送的数据为字节数组(第一个参数buf)中,length 为数据长度,address为目标主机的地址,port为目标主机的端口号。

DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)

用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从offset到 length 为数据长度,address为目标主机的地址,port为目标主机的端口号。

DatagramPacket(byte[] buf, int length, SocketAddress address) 用来发送数据报,发送的数据为字节数组(第一个参数buf)中,length 为数据长度。

DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)

构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从offset到指定长度length。address指定目的主机的IP和端口号

DatagramPacket方法

方法签名 方法说明
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData() 获取数据报中的数据

2、使用 UDP Socket 实现简单通信

下面我们在 Java 中使用 UDP 协议实现的一个简单的客户端和服务端的通信。

下面的 服务器-客户端 代码看起来挺复杂,其实和数据库中的 JDBC 差不多,都是固定的套路,尽管之后在写更复杂的 服务器-客户端 程序,也都是在这个基础上拓展。例如下面的 UDP Socket 通过传输层实现网络通信,代码无非就下面几个步骤:


1.对于服务器:

(1)读取请求并解析

(2)根据请求计算响应

(3)把响应结果发送到客户端

2.对于客户端:

(1)构造并发送请求

(2)接收服务器返回的响应并解析响应

服务器程序:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// Echo-回显服务器。客户端发了个请求,服务器返回一个一模一样的响应。
public class UdpEchoSever {
    // 需要先定义一个 socket 对象,使用网络通信,必须要使用 socekt 对象
    private DatagramSocket socket = null;
    // 绑定一个端口号,不一定能成功,比如某个端口号已经被别的进程占用了,此时这里的绑定操作就会出错。
    // 需要注意的是:同一个主机上,一个端口,同一时刻,只能被一个进程绑定。
    public UdpEchoSever(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    // 启动服务器主逻辑
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 每次循环,做三件事
            // 1. 读取请求并解析
            //    构造一个空的DatagramPacket对象,用来接收客户端请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //    从网卡上接收请求 此处的 requestPacket 为输出型参数
            socket.receive(requestPacket);
            //    这里为了方便处理这个请求,将数据包转化为 String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            // 2. 根据请求计算响应
            String response = process(request);
            // 3. 把响应结果写回到客户端
            //    根据 response 字符串,构造一个 DatagramPacket
            //    和请求 packet 不同,此处构造响应的时候,需要指定这个包要发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    // requestPacket 是从客户端这里收来的,getSocketAddress 会得到客户端的 ip 何为端口
                    requestPacket.getSocketAddress());
                    
            socket.send(responsePacket);
            // 方面查看,打印一下日志
            // ip 和 端口号 + 请求内容 + 响应内容
            System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }

    // process是请求处理方法,这是服务器中的一个关键环节!!!
    public String process(String request) {
        return request;
    }
    
  // 主函数
    public static void main(String[] args) throws IOException {
        UdpEchoSever udpEchoSever = new UdpEchoSever(9090);
        udpEchoSever.start();
    }
}

客户端程序:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    // 客户端启动, 需要知道服务器在哪里!!
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 对于客户端来说, 不需要显示关联端口.
        // 不代表没有端口, 而是系统自动分配了个空闲的端口.
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        // 通过这个客户端可以多次和服务器进行交互.
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台, 读取一个字符串过来
            //    先打印一个提示符, 提示用户要输入内容
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 把字符串构造成 UDP packet, 并进行发送.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            // 3. 客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 4. 把响应数据转换成 String 显示出来.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
      // 127.0.0.1 是一个特殊的IP地址,表示本机的回环地址。
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

对于UDP Echo Sever 来说,socket 对象的生命周期伴随整个程序的不需要 close。这个 socket 对象是出了循环就不用了,但是循环结束就意味着 start 结束,意味着 main 方法结束,意味着进程结束,进程结束所有文件资源就自动释放了。

三、TCP 流套接字编程

1、TCP 流套接字编程 API

(1)SeverSocket

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

ServerSocket构造方法

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

ServerSocket 方法

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

(2)Socket

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

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

Socket 构造方法

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

Socket 方法

方法签名 方法说明
int getPort() 返回此套接字连接到的远程端口号
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流
void close() 关闭此套接字

2、使用 TCP Socket 实现简单通信

下面我们同样写一个简单的基于 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.ExecutorService;
import java.util.concurrent.Executors;

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("服务器启动!");
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            // 如果直接调用,该方法会影响这个循环的二次执行,导致 accept 不及时
            // 创建新线程,用新线程调用 processConnection
            // 每次来一个新的客户端都创建一个新线程

            // 1.方案一:每次创建线程(每次创建销毁,开销较大)
//            Thread t = new Thread(()->{
//                processConnection(clientSocket);
//            });
//            t.start();

            // 2.方案二:使用线程池
            pool.submit(()->{
                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 () 这种写法,( ) 中允许写多个流对象,使用 ; 分割
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 为了简单,把字节流包装成了更方便的字符流
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            // 一次可能发来多个请求,这里规定以 \n 为分隔符
            while (true) {
                // 1.读取请求
                // 特殊处理一下:
                if (!scanner.hasNext()) {
                    // 读取的流到了结尾(对端关闭了)
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                // 直接使用 scanner 读取一段字符串
                String request = scanner.next();
                // 2.根据请求计算响应
                String response = process(request);
                // 3.把响应写会给客户端,不要忘了,响应里也是要带上换行的
                printWriter.println(response);
                // 写网卡为全缓冲,这里使用flush刷新
                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();
        }
    }
    
    // 处理请求
    public String process(String request) {
        return request;
    }
  // 主方法
    public static void main(String[] args) throws IOException {
        TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
        tcpEchoSever.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;

// idea 中默认一个程序只能启动一个,启动多个客户端可配置一下 IDEA。

public class TcpEchoClient {
    private Socket socket = null;

    // ***只有这里会建立连接,和 Udp 不同***
    public TcpEchoClient(String severIp, int port) throws IOException {
        // 这个操作就相当于让客户端和服务器建立 TCP 连接
        // 这里的链接连上了,accept 就会返回
        socket = new Socket(severIp,port);
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            // 将字节流包装成字符流
            Scanner scannerFromSocket = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                // 1.从键盘上读取用户输入的内容
                System.out.print("->");
                String request = scanner.next();
                // 2.把读取的内容构成请求,发给服务器
                //   注意:这里的发送,是带换行的!
                printWriter.println(request);
                // 写网卡为全缓冲,这里使用flush刷新
                printWriter.flush();
                // 3.从服务器读取响应内容
                String response = scannerFromSocket.next();
                // 4. 把响应的结果显示到控制台上
                System.out.printf("req: %s ; resq: %s\n",request,response);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  // 主方法
    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }

}

3、使用 Tcp 协议进行网络传输的“五大要点”

(1)自定义简单的应用层协议

对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式,上述为了简单,做了如下简单约定:

  1. 每个请求是个字符串
  2. 请求和请求之间,使用\n(换行符)分割

由于是回显服务器,响应和请求是一模一样的,因此也遵循上述规则。

(2)写网卡是全缓冲(写文件也是全缓冲)

为了提高IO效率,引入了缓冲区,使用缓冲区可以减少IO次数,提高整体的效率。上述 printWriter.println(“内容”) 过后,内容就被写入到了缓冲区,如果不刷新缓冲区,就要等到缓冲区满,自动刷新到网卡中,所以执行上述程序可能就会出现只请求不响应的情况,为了解决这个问题,我们可以在每次写网卡后,手动进行刷新:printWriter.flush()


(3)长连接 与 短连接

长连接和短连接是指在网络编程中不同的连接方式。


短连接指客户端与服务器建立连接后,在完成一次请求-响应操作之后就会断开连接。每次请求都需要重新建立连接,这种方式可以保证连接使用的资源较少,但也对服务器的压力较大。常用于小数据量的频繁通信场景,例如HTTP协议。


而长连接则是指客户端与服务器建立连接后,在一段时间内可以保持连接状态,多次请求-响应操作共用这一个连接。这种方式相对于短连接可以减少连接建立、关闭的次数,提高了通信效率,但是缺点是需要维护连接的状态,如果长时间没有交互,则需要进行心跳检测等机制来维持连接状态。常用于对实时性要求较高的通信场景,例如即时通讯、游戏等。

在上述TCP协议中使用到长连接。


(4)使用多线程

上述例子的服务器中使用到了多线程,如果不使用多线程,代码可能产生 BUG。因为上述 start 的 while 循环,是用来循环的接收连接,而下面的 processConnection 内部也有一个循环用来循环的处理连接。假设现在来了一个连接,start 方法接收连接后其中的 processConnection 就开始循环的处理这个连接,直到这个连接关闭,但是如果这个期间又有别的客户端进行新的连接,由于当前start中的第一次循环还没结束,就会导致一直阻塞,使其他连接处理不及时。为了解决上述问题,一个很好的办法就是使用多线程,为每个连接都分配一个线程独立处理。


(5)频繁创建,生命周期又短资源的需要 close 及时释放

像上述使用 UDP 协议进行网络通信这种,生命周期伴随整个程序的不需要 close。

在这里,使用 TCP 进行网络通信时,服务器那里的每个 Socket 对象只是给一个连接提供服务的,可能会有很多个连接。在这种情况下,服务器会为每个连接都创建一个新的 Socket 对象,作为后续通信的基础。当这个连接不再需要服务时,需要将相应的 Socket 对象关闭,以便及时释放资源。


相关文章
|
4月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
在数字时代,网络应用成为连接世界的桥梁。Python凭借简洁的语法和丰富的库支持,成为开发高效网络应用的首选。本文通过实时聊天室案例,介绍Python Socket编程的基础与进阶技巧。基础篇涵盖服务器和客户端的建立与数据交换;进阶篇则探讨多线程与异步IO优化方案,助力提升应用性能。通过本案例,你将掌握Socket编程的核心技能,推动网络应用飞得更高、更远。
74 1
|
2月前
|
Kubernetes 网络协议 Python
Python网络编程:从Socket到Web应用
在信息时代,网络编程是软件开发的重要组成部分。Python作为多用途编程语言,提供了从Socket编程到Web应用开发的强大支持。本文将从基础的Socket编程入手,逐步深入到复杂的Web应用开发,涵盖Flask、Django等框架的应用,以及异步Web编程和微服务架构。通过本文,读者将全面了解Python在网络编程领域的应用。
45 1
|
3月前
|
消息中间件 监控 网络协议
Python中的Socket魔法:如何利用socket模块构建强大的网络通信
本文介绍了Python的`socket`模块,讲解了其基本概念、语法和使用方法。通过简单的TCP服务器和客户端示例,展示了如何创建、绑定、监听、接受连接及发送/接收数据。进一步探讨了多用户聊天室的实现,并介绍了非阻塞IO和多路复用技术以提高并发处理能力。最后,讨论了`socket`模块在现代网络编程中的应用及其与其他通信方式的关系。
342 3
|
3月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
4月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
在数字宇宙中,网络如同复杂脉络连接每个角落,Python Socket编程则是开启这一世界的钥匙。本文将引导你从基础概念入手,逐步掌握Socket编程,并通过实战示例构建TCP/UDP服务器与客户端。你将学会使用Python的socket模块进行网络通信,了解TCP与UDP的区别,并运用多线程与异步IO提升服务器性能。跟随本文指引,成为网络世界的建筑师,构建自己的网络帝国。
41 2
|
4月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
67 3
|
4月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
在网络技术飞速发展的今天,掌握网络编程已成为开发者的重要技能。本文以Python为工具,带你从Socket编程基础逐步深入至进阶领域。首先介绍Socket的概念及TCP/UDP协议,接着演示如何用Python创建、绑定、监听Socket,实现数据收发;最后通过构建简单的聊天服务器,巩固所学知识。让初学者也能迅速上手,成为网络编程高手。
86 1
|
4月前
|
网络协议 安全 网络安全
震惊!Python Socket竟能如此玩转网络通信,基础到进阶全攻略!
【9月更文挑战第12天】在网络通信中,Socket编程是连接不同应用与服务的基石。本文通过问答形式,从基础到进阶全面解析Python Socket编程。涵盖Socket的重要性、创建TCP服务器与客户端、处理并发连接及进阶话题如非阻塞Socket、IO多路复用等,帮助读者深入了解并掌握网络通信的核心技术。
161 6
|
4月前
|
消息中间件 网络协议 网络安全
解锁Python Socket新姿势,进阶篇带你玩转高级网络通信技巧!
【9月更文挑战第13天】在掌握了Python Socket编程基础后,你是否想进一步提升技能?本指南将深入探讨Socket编程精髓,包括从阻塞到非阻塞I/O以提高并发性能,使用`select`进行非阻塞操作示例;通过SSL/TLS加密通信保障数据安全,附带创建SSL服务器的代码实例;以及介绍高级网络协议与框架,如HTTP、WebSocket和ZeroMQ,帮助你简化复杂应用开发。通过学习这些高级技巧,你将在网络编程领域更进一步。
52 3
|
4月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。