TCP Socket编程基础实战

简单的TCP服务器和客户端

这段代码实现了一个TCP回显客户端,通过套接字连接到指定的服务器,并发送一个字符串。它会等待服务器返回相同的字符串,然后将其打印出来。

  • 创建Socket对象: 使用 Socket socket = new Socket(server, servPort); 这行代码创建了一个 Socket 对象,并指定了连接的目标服务器和端口号。
  • TCP套接字编程: 使用 Socket 类来建立与服务器的TCP连接,并通过 getInputStream() 和 getOutputStream() 获取输入输出流进行数据交换。
  • 数据传输和接收: 使用输入流 InputStream 从服务器接收数据,并使用输出流 OutputStream 向服务器发送数据。

TCPEchoClient.java

java 复制代码
import java.io.*;
import java.net.*;

public class TCPEchoClient {

    public static void main(String[] args) throws IOException {
        // 应用程序设置和参数解析
        if ((args.length < 2) || (args.length > 3)) // 检查参数个数是否正确
            throw new IllegalArgumentException("参数应为: <服务器> <字符串> [<端口>]");
        
        String server = args[0]; // 服务器名或IP地址
        // 使用默认字符编码将字符串参数转换为字节数组
        byte[] data = args[1].getBytes();
        
        int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7; // 确定回显服务器的端口号

        // 创建TCP套接字
        Socket socket = new Socket(server, servPort); // 创建套接字并连接到指定端口的服务器
        System.out.println("已连接到服务器...正在发送回显字符串");

        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        out.write(data); // 发送编码后的字符串到服务器

        // 接收服务器的回复
        int totalBytesRcvd = 0; // 目前为止收到的总字节数
        int bytesRcvd; // 上一次读取的字节数
        while (totalBytesRcvd < data.length) {
            // 从输入流中读取数据到 'data' 数组中
            if ((bytesRcvd = in.read(data, totalBytesRcvd, data.length - totalBytesRcvd)) == -1)
                throw new SocketException("连接意外关闭");
            totalBytesRcvd += bytesRcvd;
        } // 数据数组已填满

        // 打印接收到的回显字符串
        System.out.println("接收到的回显: " + new String(data));

        socket.close(); // 关闭套接字及其流
    }
}

这段代码实现了一个基于TCP协议的简单回显服务器,它能够接收客户端的连接请求,将客户端发送的数据原样返回给客户端。

  • 创建服务器套接字: 使用 ServerSocket servSock = new ServerSocket(servPort); 创建一个服务器套接字,并指定监听的端口号。
  • 接收客户端连接: 使用 servSock.accept() 方法阻塞等待客户端的连接请求,一旦连接建立,返回一个新的 Socket 对象 clntSock,用于与客户端通信。
  • 处理数据交换: 使用输入流 InputStream 从客户端接收数据,将其存储到 receiveBuf 缓冲区中;然后使用输出流 OutputStream 将缓冲区中的数据写回客户端。
  • 释放资源: 在处理完客户端请求后,调用 clntSock.close() 关闭客户端套接字,释放相关资源。

TCPEchoServer.java

java 复制代码
// 导入必要的包 java.net.* 和 java.io.* 用于网络和 I/O 操作
import java.net.*; // for Socket, ServerSocket, and InetAddress
import java.io.*;  // for IOException and Input/OutputStream

// 定义 TCPEchoServer 类
public class TCPEchoServer {

    // 定义接收缓冲区的大小为 32 字节
    private static final int BUFSIZE = 32;

    // 主方法,程序入口
    public static void main(String[] args) throws IOException {
        // 确认传入的参数个数正确
        if (args.length != 1)
            throw new IllegalArgumentException("Parameter(s): <Port>");

        // 从命令行参数获取服务器端口号
        int servPort = Integer.parseInt(args[0]);

        // 创建一个服务器套接字来接收客户端的连接请求
        ServerSocket servSock = new ServerSocket(servPort);

        int recvMsgSize; // 接收消息的大小
        byte[] receiveBuf = new byte[BUFSIZE]; // 接收缓冲区

        // 进入无限循环,持续接收和处理客户端的连接
        while (true) {
            // 接收客户端连接(阻塞直到有连接到来)
            Socket clntSock = servSock.accept();

            // 获取客户端的地址和端口信息
            SocketAddress clientAddress = clntSock.getRemoteSocketAddress();
            System.out.println("Handling client at " + clientAddress); // 打印客户端的地址和端口信息

            // 获取客户端套接字的输入流和输出流
            InputStream in = clntSock.getInputStream();
            OutputStream out = clntSock.getOutputStream();

            // 接收并返回数据,直到客户端关闭连接(in.read()返回-1)
            while ((recvMsgSize = in.read(receiveBuf)) != -1) {
                // 将接收到的数据写回给客户端
                out.write(receiveBuf, 0, recvMsgSize);
            }

            // 关闭客户端套接字,释放资源
            clntSock.close();
        }
        /* NOT REACHED */
    }
}

帧处理器

这段代码实现了一个基于分隔符的消息传输协议处理类 DelimFramer,用于在消息中添加分隔符并从输入流中提取消息。

  • frameMsg 方法: 将消息添加分隔符并写入输出流。如果消息中包含分隔符,会抛出 IOException。
  • nextMsg 方法: 从输入流中读取字节,直到遇到分隔符 \n。如果遇到输入流末尾但未找到分隔符,会抛出 EOFException。

DelimFramer.java

java 复制代码
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class DelimFramer implements Framer {

    private InputStream in; // 数据源
    private static final byte DELIMITER = '\n'; // 消息分隔符,换行符

    // 构造函数,接受一个输入流作为参数
    public DelimFramer(InputStream in) {
        this.in = in;
    }

    // 将消息添加帧信息并写入输出流
    public void frameMsg(byte[] message, OutputStream out) throws IOException {
        // 确保消息中不包含分隔符
        for (byte b : message) {
            if (b == DELIMITER) {
                // 如果消息中包含分隔符,抛出IOException
                throw new IOException("Message contains delimiter");
            }
        }
        // 写入消息字节数组到输出流
        out.write(message);
        // 写入分隔符到输出流,表示消息结束
        out.write(DELIMITER);
        // 刷新输出流,确保所有数据被发送
        out.flush();
    }

    // 从输入流中提取消息
    public byte[] nextMsg() throws IOException {
        // 用于存储消息的缓冲区
        ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
        int nextByte;

        // 逐字节读取输入流,直到找到分隔符
        while ((nextByte = in.read()) != DELIMITER) {
            // 如果到达输入流末尾
            if (nextByte == -1) {
                // 如果缓冲区为空,返回null,表示所有消息都已接收
                if (messageBuffer.size() == 0) {
                    return null;
                } else {
                    // 如果缓冲区非空但未找到分隔符,抛出EOFException,表示帧错误
                    throw new EOFException("Non-empty message without delimiter");
                }
            }
            // 将读取到的字节写入缓冲区
            messageBuffer.write(nextByte);
        }

        // 返回缓冲区内容作为字节数组
        return messageBuffer.toByteArray();
    }
}

这段代码实现了另一种消息帧处理器 LengthFramer,它使用消息长度作为消息的前缀,并从输入流中读取指定长度的消息内容。

LengthFramer.java

java 复制代码
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class LengthFramer implements Framer {
    public static final int MAXMESSAGELENGTH = 65535;
    public static final int BYTEMASK = 0xff;
    public static final int SHORTMASK = 0xffff;
    public static final int BYTESHIFT = 8;

    private DataInputStream in; // 数据输入流的包装器

    // 构造方法,初始化输入流的 DataInputStream 包装器
    public LengthFramer(InputStream in) throws IOException {
        this.in = new DataInputStream(in);
    }

    // 帧化消息的方法
    public void frameMsg(byte[] message, OutputStream out) throws IOException {
        // 验证消息长度是否合法
        if (message.length > MAXMESSAGELENGTH) {
            throw new IOException("message too long");
        }

        // 写入长度前缀
        out.write((message.length >> BYTESHIFT) & BYTEMASK); // 写入高位字节
        out.write(message.length & BYTEMASK); // 写入低位字节

        // 写入消息内容
        out.write(message); // 写入消息主体
        out.flush(); // 确保数据立即发送
    }

    // 从输入流中提取下一个帧化消息的方法
    public byte[] nextMsg() throws IOException {
        int length;
        try {
            length = in.readUnsignedShort(); // 读取两字节作为消息长度
        } catch (EOFException e) {
            // 如果遇到流结束异常,返回 null 表示没有消息可读取
            return null;
        }

        // 创建一个字节数组,用于存储消息内容
        byte[] msg = new byte[length];

        // 从输入流中读取指定长度的消息内容
        in.readFully(msg); // 阻塞直到读满指定长度的消息内容,否则抛出异常

        // 返回读取的消息内容
        return msg;
    }
}

TCP编程综合示例

投票类

这段代码定义了一个 VoteMsg 类,用于表示投票消息和响应消息的数据结构和行为。

  • 构造方法: 初始化消息类型、候选人ID和投票数,同时验证参数的合法性。
  • 数据访问方法: 提供了设置和获取消息类型、候选人ID以及投票数的方法,同时进行了参数的有效性检查。
  • 异常处理: 在构造方法和设置方法中使用了异常来处理不合法的参数输入,确保对象的一致性和安全性。
  • toString 方法: 将 VoteMsg 对象转换为易读的字符串表示形式,方便打印和调试。

VoteMsg.java

java 复制代码
public class VoteMsg {
    private boolean isInquiry; // 是否为查询消息,true 表示查询,false 表示投票
    private boolean isResponse; // 是否为服务器的响应消息,true 表示响应消息
    private int candidateID; // 候选人ID,范围在 [0, 1000] 之间
    private long voteCount; // 在响应中表示获得的票数,仅在 isResponse 为 true 时有效

    public static final int MAX_CANDIDATE_ID = 1000; // 最大候选人ID

    // 构造方法,初始化 VoteMsg 实例
    public VoteMsg(boolean isResponse, boolean isInquiry, int candidateID, long voteCount)
            throws IllegalArgumentException {
        // 检查不变量
        if (voteCount != 0 && !isResponse) {
            throw new IllegalArgumentException("Request vote count must be zero");
        }
        if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
            throw new IllegalArgumentException("Bad Candidate ID: " + candidateID);
        }
        if (voteCount < 0) {
            throw new IllegalArgumentException("Total must be >= zero");
        }
        
        // 初始化实例变量
        this.candidateID = candidateID;
        this.isResponse = isResponse;
        this.isInquiry = isInquiry;
        this.voteCount = voteCount;
    }

    // 设置是否为查询消息
    public void setInquiry(boolean isInquiry) {
        this.isInquiry = isInquiry;
    }

    // 设置是否为响应消息
    public void setResponse(boolean isResponse) {
        this.isResponse = isResponse;
    }

    // 获取是否为查询消息
    public boolean isInquiry() {
        return isInquiry;
    }

    // 获取是否为响应消息
    public boolean isResponse() {
        return isResponse;
    }

    // 设置候选人ID,如果不合法则抛出异常
    public void setCandidateID(int candidateID) throws IllegalArgumentException {
        if (candidateID < 0 || candidateID > MAX_CANDIDATE_ID) {
            throw new IllegalArgumentException("Bad Candidate ID: " + candidateID);
        }
        this.candidateID = candidateID;
    }

    // 获取候选人ID
    public int getCandidateID() {
        return candidateID;
    }

    // 设置投票数,如果不合法则抛出异常
    public void setVoteCount(long count) {
        if ((count != 0 && !isResponse) || count < 0) {
            throw new IllegalArgumentException("Bad vote count");
        }
        voteCount = count;
    }

    // 获取投票数
    public long getVoteCount() {
        return voteCount;
    }

    // 将 VoteMsg 对象转换成字符串表示形式
    @Override
    public String toString() {
        String res = (isInquiry ? "inquiry" : "vote") + " for candidate " + candidateID;
        if (isResponse) {
            res = "response to " + res + " who now has " + voteCount + " vote(s)";
        }
        return res;
    }
}

编码器类

这段代码实现了一个 VoteMsgCoder 类,用于将 VoteMsg 对象编码为字节数组并解码回 VoteMsg 对象的功能。

  • 功能: 将 VoteMsg 对象转换为字节数组,以便进行网络传输或持久化存储。
  • 实现: 使用 ByteArrayOutputStream 和 DataOutputStream 将消息的标志位和投票数写入字节流中。
  • 标志位设置: 根据 VoteMsg 对象的状态(是否为响应、是否为查询)设置标志位,并将候选人ID左移两位腾出空间存放标志位。
  • 异常处理: 在写入数据时,通过 IOException 处理可能出现的异常。

VoteMsgCoder.java

java 复制代码
import java.io.*;

public class VoteMsgCoder {
    // 编码中使用的常量
    private static final int RESPONSE_FLAG = 0x0100; // 响应标志位
    private static final int INQUIRY_FLAG = 0x0200;  // 查询标志位
    private static final int RESERVED_FLAG = 0xFC00; // 保留位

    private static final int MAX_CANDIDATE_ID = 1000; // 最大候选人ID

    // 编码方法:将VoteMsg编码为字节数组
    public byte[] encode(VoteMsg msg) throws IOException {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(byteStream);

        // 设置标志位
        short flags = 0;
        if (msg.isResponse()) flags |= RESPONSE_FLAG;
        if (msg.isInquiry()) flags |= INQUIRY_FLAG;
        
        // 将候选人ID左移两位以腾出空间存放标志位
        flags |= (msg.getCandidateID() << 2);

        // 写入标志位和投票数到输出流
        out.writeShort(flags);
        out.writeLong(msg.getVoteCount());

        out.flush();
        return byteStream.toByteArray();
    }

    // 解码方法:将字节数组解码为VoteMsg对象
    public VoteMsg decode(byte[] data) throws IOException {
        ByteArrayInputStream byteStream = new ByteArrayInputStream(data);
        DataInputStream in = new DataInputStream(byteStream);

        // 读取标志位
        short flags = in.readShort();
        boolean isResponse = (flags & RESPONSE_FLAG) != 0; // 检查是否为响应
        boolean isInquiry = (flags & INQUIRY_FLAG) != 0;  // 检查是否为查询
        int candidateID = (flags >> 2) & MAX_CANDIDATE_ID; // 读取候选人ID
        long voteCount = in.readLong(); // 读取投票数

        // 构造并返回VoteMsg对象
        return new VoteMsg(isResponse, isInquiry, candidateID, voteCount);
    }
}

VoteMsgTextCoder 类实现了 VoteMsgCoder 接口,用于将 VoteMsg 对象编码为特定格式的文本字符串(文本协议),并从文本字符串解码回 VoteMsg 对象。

  • 功能: 将 VoteMsg 对象编码为字节数组,采用特定的文本格式。
  • 实现: 将 VoteMsg 对象的信息按照预定义的格式拼接成字符串,并将其转换为字节数组。
  • 字段包括: 包括魔术字符串 "Voting"、查询/投票指示符、响应标志、候选人ID以及投票数。
  • 字符集: 使用固定的字符集 "US-ASCII" 将字符串转换为字节数组。

VoteMsgTextCoder.java

ini 复制代码
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;

public class VoteMsgTextCoder implements VoteMsgCoder {
    /*
    * Wire Format "VOTEPROTO" <"v"|"i"> [<RESPFLAG>] <CANDIDATE> [<VOTECNT>]
    * Charset is fixed by the wire format.
    */
    
    // 编码用的常量
    public static final String MAGIC = "Voting";
    public static final String VOTESTR = "v";
    public static final String INQSTR = "i";
    public static final String RESPONSESTR = "R";
    
    public static final String CHARSETNAME = "US-ASCII";
    public static final String DELIMSTR = " ";
    public static final int MAX_WIRE_LENGTH = 2000;
    
    // 将 VoteMsg 对象编码为字节数组
    public byte[] toWire(VoteMsg msg) throws IOException {
        // 构建消息字符串
        String msgString = MAGIC + DELIMSTR + (msg.isInquiry() ? INQSTR : VOTESTR)
                + DELIMSTR + (msg.isResponse() ? RESPONSESTR + DELIMSTR : "")
                + Integer.toString(msg.getCandidateID()) + DELIMSTR
                + Long.toString(msg.getVoteCount());
        // 将字符串转换为字节数组
        byte data[] = msgString.getBytes(CHARSETNAME);
        return data;
    }
    
    // 从字节数组解码为 VoteMsg 对象
    public VoteMsg fromWire(byte[] message) throws IOException {
        ByteArrayInputStream msgStream = new ByteArrayInputStream(message);
        Scanner s = new Scanner(new InputStreamReader(msgStream, CHARSETNAME));
        boolean isInquiry;
        boolean isResponse;
        int candidateID;
        long voteCount;
        String token;
        
        try {
            token = s.next();
            if (!token.equals(MAGIC)) {
                throw new IOException("Bad magic string: " + token);
            }
            token = s.next();
            if (token.equals(VOTESTR)) {
                isInquiry = false;
            } else if (!token.equals(INQSTR)) {
                throw new IOException("Bad vote/inq indicator: " + token);
            } else {
                isInquiry = true;
            }
            
            token = s.next();
            if (token.equals(RESPONSESTR)) {
                isResponse = true;
                token = s.next();
            } else {
                isResponse = false;
            }
            // 当前 token 是 candidateID
            // 注意:isResponse 现在有效
            candidateID = Integer.parseInt(token);
            if (isResponse) {
                token = s.next();
                voteCount = Long.parseLong(token);
            } else {
                voteCount = 0;
            }
        } catch (IOException ioe) {
            throw new IOException("Parse error...");
        }
        return new VoteMsg(isResponse, isInquiry, candidateID, voteCount);
    }
}

这段代码实现了一个 VoteMsgBinCoder 类,用于将 VoteMsg 对象编码为特定的二进制格式,并从二进制数据解码回 VoteMsg 对象。

  • 功能: 将 VoteMsg 对象编码为二进制格式的字节数组。
  • 实现: 使用 DataOutputStream 将 VoteMsg 对象的属性按照预定义的格式写入字节流中。
  • 字段包括: 包括魔术数 MAGIC、标志位 FLAGS(响应标志和查询标志)、候选人ID以及投票数(仅在响应消息中存在)。
  • 流操作: 使用 writeShort 和 writeLong 方法写入 short 和 long 类型的数据,并通过 flush 立即发送数据。

VoteMsgBinCoder.java

java 复制代码
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/* Wire Format
 * 1 1 1 1 1 1
 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * | Magic |Flags| ZERO |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * | Candidate ID |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * | |
 * | Vote Count (only in response) |
 * | |
 * | |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 */
public class VoteMsgBinCoder implements VoteMsgCoder {

    // 编码用的常量
    public static final int MIN_WIRE_LENGTH = 4;
    public static final int MAX_WIRE_LENGTH = 16;
    public static final int MAGIC = 0x5400;
    public static final int MAGIC_MASK = 0xfc00;
    public static final int MAGIC_SHIFT = 8;
    public static final int RESPONSE_FLAG = 0x0200;
    public static final int INQUIRE_FLAG = 0x0100;

    // 将 VoteMsg 对象编码为字节数组
    public byte[] toWire(VoteMsg msg) throws IOException {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(byteStream); // 将数据转换为二进制

        // 构建 magic 和 flags
        short magicAndFlags = MAGIC;
        if (msg.isInquiry()) {
            magicAndFlags |= INQUIRE_FLAG;
        }
        if (msg.isResponse()) {
            magicAndFlags |= RESPONSE_FLAG;
        }
        out.writeShort(magicAndFlags); // 写入 magic 和 flags
        // 我们知道 candidate ID 可以放进一个 short 类型:它在 0 和 1000 之间
        out.writeShort((short) msg.getCandidateID()); // 写入 candidate ID
        if (msg.isResponse()) {
            out.writeLong(msg.getVoteCount()); // 如果是响应消息,写入 vote count
        }
        out.flush();
        byte[] data = byteStream.toByteArray();
        return data;
    }

    // 从字节数组解码为 VoteMsg 对象
    public VoteMsg fromWire(byte[] input) throws IOException {
        // 检查消息长度
        if (input.length < MIN_WIRE_LENGTH) {
            throw new IOException("Runt message");
        }
        ByteArrayInputStream bs = new ByteArrayInputStream(input);
        DataInputStream in = new DataInputStream(bs);
        int magic = in.readShort(); // 读取 magic 和 flags
        if ((magic & MAGIC_MASK) != MAGIC) {
            throw new IOException("Bad Magic #: " +
                ((magic & MAGIC_MASK) >> MAGIC_SHIFT));
        }
        boolean resp = ((magic & RESPONSE_FLAG) != 0);
        boolean inq = ((magic & INQUIRE_FLAG) != 0);
        int candidateID = in.readShort(); // 读取 candidate ID
        if (candidateID < 0 || candidateID > 1000) {
            throw new IOException("Bad candidate ID: " + candidateID);
        }
        long count = 0;
        if (resp) {
            count = in.readLong(); // 如果是响应消息,读取 vote count
            if (count < 0) {
                throw new IOException("Bad vote count: " + count);
            }
        }
        // 忽略任何额外的字节
        return new VoteMsg(resp, inq, candidateID, count);
    }
}

投票操作类

这段代码定义了一个 VoteService 类,用于处理投票请求,并维护候选人的投票计数。

结果映射 (results):

  • 使用 Map<Integer, Long> 来存储候选人的投票计数,其中键是候选人的ID,值是投票的数量。

handleRequest 方法:

输入参数:接收一个 VoteMsg 对象作为输入,表示投票请求。

处理逻辑:

  • 响应检查:首先检查消息是否已经是响应类型,如果是,则直接返回原始消息。
  • 设置响应类型:将消息类型设置为响应,确保每个请求在处理后都被标记为响应。
  • 获取投票计数:从 results 映射中根据候选人ID获取当前的投票计数。
  • 处理投票:
    • 如果消息不是查询类型 (isInquiry() 返回 false),则表示是投票请求,将该候选人的投票计数加一,并更新到 results 映射中。
  • 设置投票计数:将最新的投票计数设置回 VoteMsg 对象中。

返回值:返回处理后的 VoteMsg 对象,其中包含了更新后的投票计数和标记为响应的消息类型。

VoteService.java

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class VoteService {

    // 用于存储候选人的投票计数的映射
    private Map<Integer, Long> results = new HashMap<Integer, Long>();

    // 处理投票请求的方法
    public VoteMsg handleRequest(VoteMsg msg) {
        if (msg.isResponse()) { // 如果消息已经是响应类型,直接返回
            return msg;
        }
        msg.setResponse(true); // 将消息设置为响应类型
        // 获取候选人ID和当前投票计数
        int candidate = msg.getCandidateID();
        Long count = results.get(candidate);
        if (count == null) {
            count = 0L; // 如果候选人不存在,投票计数设为0
        }
        if (!msg.isInquiry()) { // 如果不是查询类型的消息
            results.put(candidate, ++count); // 投票计数加1,并存回映射
        }
        msg.setVoteCount(count); // 设置投票计数
        return msg; // 返回处理后的消息
    }
}

TCP操作类

这段代码是一个简单的投票客户端实现,使用 TCP 协议与服务器通信。

命令行参数检查:

  • 首先检查命令行参数的数量是否正确,需要提供服务器地址和端口号。

建立连接:

  • 根据命令行参数创建一个 Socket 对象,连接到指定的服务器地址和端口。

获取流:

  • 通过 Socket 获取输出流 OutputStream,用于向服务器发送数据。

编码器和帧处理器选择:

  • 创建了一个 VoteMsgBinCoder 对象作为编码器,用于将 VoteMsg 对象编码为字节数组。
  • 创建了一个 LengthFramer 对象作为帧处理器,用于将消息帧化后发送到输出流。

发送查询请求:

  • 创建一个查询请求的 VoteMsg 对象,并使用编码器将其编码为字节数组。
  • 使用帧处理器将编码后的消息发送到服务器。

发送投票请求:

  • 将上述查询请求对象的类型改为投票请求,并再次使用编码器将其编码为字节数组。
  • 使用帧处理器再次将编码后的消息发送到服务器。

接收响应:

  • 使用帧处理器从输入流中接收并解析服务器发送的消息。
  • 使用编码器将接收到的字节数组解码为 VoteMsg 对象,并打印出接收到的响应消息。

关闭连接:

  • 在所有通信结束后关闭套接字,释放资源。

VoteClientTCP.java

java 复制代码
import java.io.OutputStream;
import java.net.Socket;

public class VoteClientTCP {

    public static final int CANDIDATEID = 888;

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

        if (args.length != 2) { // 检查参数数量是否正确
            throw new IllegalArgumentException("Parameter(s): <Server> <Port>");
        }

        String destAddr = args[0]; // 目标地址
        int destPort = Integer.parseInt(args[1]); // 目标端口

        Socket sock = new Socket(destAddr, destPort); // 创建套接字并连接到服务器
        OutputStream out = sock.getOutputStream(); // 获取输出流

        // 选择使用二进制编码器和长度帧处理器
        VoteMsgCoder coder = new VoteMsgBinCoder();
        Framer framer = new LengthFramer(sock.getInputStream());

        // 创建一个查询请求(第二个参数为true表示查询)
        VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0);
        byte[] encodedMsg = coder.toWire(msg); // 编码消息

        // 发送查询请求
        System.out.println("Sending Inquiry (" + encodedMsg.length + " bytes): ");
        System.out.println(msg);
        framer.frameMsg(encodedMsg, out); // 帧处理并发送消息

        // 现在发送一个投票请求
        msg.setInquiry(false); // 设置为投票请求
        encodedMsg = coder.toWire(msg); // 编码消息
        System.out.println("Sending Vote (" + encodedMsg.length + " bytes): ");
        framer.frameMsg(encodedMsg, out); // 帧处理并发送消息

        // 接收查询响应
        encodedMsg = framer.nextMsg(); // 获取下一条消息
        msg = coder.fromWire(encodedMsg); // 解码消息
        System.out.println("Received Response (" + encodedMsg.length + " bytes): ");
        System.out.println(msg);

        // 接收投票响应
        msg = coder.fromWire(framer.nextMsg()); // 获取并解码下一条消息
        System.out.println("Received Response (" + encodedMsg.length + " bytes): ");
        System.out.println(msg);

        sock.close(); // 关闭套接字
    }
}

这段代码是一个简单的投票服务器实现,使用 TCP 协议与客户端通信。

命令行参数检查:

  • 检查命令行参数的数量是否正确,仅需要指定服务器端口。

创建服务器套接字:

  • 根据命令行参数指定的端口创建 ServerSocket 对象,用于接受客户端连接。

选择编码器和服务:

  • 创建一个 VoteMsgBinCoder 对象作为编码器,用于将字节数组解码为 VoteMsg 对象,并将 VoteMsg 对象编码为字节数组。
  • 创建一个 VoteService 对象作为投票服务,用于处理投票请求并生成响应。
  • 接受客户端连接:
  • 使用 ServerSocket 的 accept() 方法等待并接受客户端的连接请求,返回一个 Socket 对象表示客户端连接。

处理客户端请求:

  • 对于每个接受的客户端连接,创建一个 LengthFramer 对象作为帧处理器,用于从客户端输入流中读取并处理帧化的消息。
  • 使用帧处理器的 nextMsg() 方法循环读取每条消息的字节数组,然后解码为 VoteMsg 对象。
  • 调用 VoteService 的 handleRequest() 方法处理 VoteMsg 对象并生成响应的 VoteMsg 对象。

发送响应:

  • 使用帧处理器的 frameMsg() 方法将响应的 VoteMsg 对象编码为字节数组,并通过客户端的输出流发送给客户端。

异常处理:

  • 使用 try-catch-finally 结构处理可能抛出的 IOException 异常,在异常发生时打印错误信息,并关闭客户端连接。

循环处理:

  • 外部的 while(true) 循环保证服务器持续接受并处理新的客户端连接,直到手动终止服务器进程。

VoteServerTCP.java

java 复制代码
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class VoteServerTCP {

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

        if (args.length != 1) { // 检查参数数量是否正确
            throw new IllegalArgumentException("Parameter(s): <Port>");
        }

        int port = Integer.parseInt(args[0]); // 接收端口

        ServerSocket servSock = new ServerSocket(port); // 创建服务器套接字并绑定到指定端口
        // 选择使用二进制编码器
        VoteMsgCoder coder = new VoteMsgBinCoder();
        VoteService service = new VoteService(); // 创建投票服务

        while (true) {
            Socket clntSock = servSock.accept(); // 接受客户端连接
            System.out.println("Handling client at " + clntSock.getRemoteSocketAddress());
            // 选择使用长度帧处理器
            Framer framer = new LengthFramer(clntSock.getInputStream());
            try {
                byte[] req;
                while ((req = framer.nextMsg()) != null) { // 获取并处理每条消息
                    System.out.println("Received message (" + req.length + " bytes)");
                    VoteMsg responseMsg = service.handleRequest(coder.fromWire(req)); // 处理请求并生成响应
                    framer.frameMsg(coder.toWire(responseMsg), clntSock.getOutputStream()); // 编码并发送响应
                }
            } catch (IOException ioe) {
                System.err.println("Error handling client: " + ioe.getMessage());
            } finally {
                System.out.println("Closing connection");
                clntSock.close(); // 关闭客户端连接
            }
        }
    }
}

时限

clntSock.setSoTimeout(timeBoundMillis);

setSoTimeout(int timeout) 是Java中Socket类的方法之一,用于设置Socket的读取超时时间。它指定了在读取数据时,如果在指定的时间内(以毫秒为单位)没有数据可读,则抛出 SocketTimeoutException 异常。这个方法在网络编程中很有用,可以避免程序无限期地阻塞等待数据到达,提高程序的健壮性和可靠性。

TimelimitEchoProtocol.java

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TimelimitEchoProtocol implements Runnable {
    private static final int BUFSIZE = 32; // 缓冲区大小(字节)
    private static final String TIMELIMIT = "10000"; // 默认时间限制(毫秒)
    private static final String TIMELIMITPROP = "Timelimit"; // 属性名

    private static int timelimit; // 时间限制(毫秒)
    private Socket clntSock; // 客户端套接字
    private Logger logger; // 日志记录器

    /**
     * 构造方法,初始化客户端套接字和日志记录器,并获取时间限制
     *
     * @param clntSock 客户端套接字
     * @param logger   日志记录器
     */
    public TimelimitEchoProtocol(Socket clntSock, Logger logger) {
        this.clntSock = clntSock;
        this.logger = logger;
        // 从系统属性获取时间限制,如果未指定,则使用默认值
        timelimit = Integer.parseInt(System.getProperty(TIMELIMITPROP, TIMELIMIT));
    }

    /**
     * 处理客户端连接的方法,实现了简单的回显协议,并设置了时间限制
     *
     * @param clntSock 客户端套接字
     * @param logger   日志记录器
     */
    public static void handleEchoClient(Socket clntSock, Logger logger) {
        try {
            // 获取套接字的输入和输出流
            InputStream in = clntSock.getInputStream();
            OutputStream out = clntSock.getOutputStream();
            int recvMsgSize; // 接收消息的大小
            int totalBytesEchoed = 0; // 从客户端接收的总字节数
            byte[] echoBuffer = new byte[BUFSIZE]; // 接收缓冲区
            long endTime = System.currentTimeMillis() + timelimit; // 计算结束时间
            int timeBoundMillis = timelimit; // 剩余时间(毫秒)

            clntSock.setSoTimeout(timeBoundMillis); // 设置套接字超时时间

            // 接收直到客户端关闭连接(通过读取-1表示),或者超过时间限制
            while ((timeBoundMillis > 0) && ((recvMsgSize = in.read(echoBuffer)) != -1)) {
                out.write(echoBuffer, 0, recvMsgSize); // 回显接收到的数据
                totalBytesEchoed += recvMsgSize; // 更新接收的总字节数
                timeBoundMillis = (int) (endTime - System.currentTimeMillis()); // 更新剩余时间
                clntSock.setSoTimeout(timeBoundMillis); // 更新套接字超时时间
            }

            // 记录客户端连接的信息和回显的字节数
            logger.info("Client " + clntSock.getRemoteSocketAddress() +
                    ", echoed " + totalBytesEchoed + " bytes.");
        } catch (IOException ex) {
            // 处理IO异常,记录警告日志
            logger.log(Level.WARNING, "Exception in echo protocol", ex);
        }
    }

    /**
     * 实现 Runnable 接口的 run 方法,在线程中运行处理客户端连接的方法
     */
    @Override
    public void run() {
        handleEchoClient(this.clntSock, this.logger); // 在线程中处理客户端连接
    }
}

单一流关闭

不建议IM业务开发中使用

问题 具体原因
1. 不可靠性 available()无法区分「流关闭」和「暂时无数据」,网络延迟会导致误判
2. 性能损耗 轮询检测会浪费CPU资源(尤其在长连接场景)
3. 协议冲突 IM协议通常自定义了关闭逻辑(如WebSocket的Close Frame),手动检测会破坏协议完整性
4. 多线程风险 检测过程中可能同时触发其他IO操作,导致状态竞争

服务器端可以通过检测输入流是否已经关闭来确定客户端是否已经调用了 shutdownOutput() 方法。在Java中,可以使用 InputStream.available() 方法来检查输入流是否还有数据可读,或者捕获 IOException 异常来判断输入流的状态。

资源管理和释放:

  • 在长时间运行的网络应用中,及时关闭不再需要的输入或输出流可以释放资源,避免资源泄漏。
  • 特别是在多线程或并发环境中,关闭不再需要的流可以避免资源竞争和冲突,提高系统稳定性和性能。

错误处理和异常控制:

  • 在处理网络异常或错误时,适时关闭不再需要的流可以清理不必要的状态或资源,帮助更好地管理程序的健壮性和可靠性。
  • 对于意外情况或特定的协议设计,关闭不需要的流可以更好地控制程序行为,避免未定义的行为或资源耗尽。

性能优化和流控制:

  • 关闭输出流可以通知远程端点停止发送数据,有助于实现流量控制和性能优化。
  • 在某些情况下,关闭输入流可以帮助程序更快地检测和处理远程端点的状态变化或关闭连接。

NIO

clntChan.connect(new InetSocketAddress(server, servPort)) 方法尝试建立到服务器的连接。如果连接不能立即建立完成(即返回 false),则表示连接过程尚未完成。

在这种情况下,进入 while 循环,调用 clntChan.finishConnect() 方法来轮询连接状态。在等待连接建立完成的过程中,程序可以执行其他操作,如打印 . 表示正在等待。

当 clntChan.finishConnect() 方法返回 true,表示连接已成功建立完成,循环结束,程序继续执行后续操作。

TCPEchoClientNonblocking.java

java 复制代码
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class TCPEchoClientNonblocking {

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

        if ((args.length < 2) || (args.length > 3)) // 检查参数数量是否正确
            throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]");

        String server = args[0]; // 获取服务器名或IP地址
        byte[] argument = args[1].getBytes(); // 将输入的字符串转换为字节数组,使用默认字符集

        int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7; // 获取服务器端口,默认为7

        // 创建 SocketChannel 并设置为非阻塞模式
        SocketChannel clntChan = SocketChannel.open();
        clntChan.configureBlocking(false);

        // 开始连接服务器,并重复轮询直到连接完成
        if (!clntChan.connect(new InetSocketAddress(server, servPort))) {
            while (!clntChan.finishConnect()) {
                System.out.print("."); // 可以执行其他操作,演示等待连接完成时的忙等待
            }
        }

        ByteBuffer writeBuf = ByteBuffer.wrap(argument); // 创建写缓冲区
        ByteBuffer readBuf = ByteBuffer.allocate(argument.length); // 创建读缓冲区

        int totalBytesRcvd = 0; // 接收到的总字节数
        int bytesRcvd; // 最后一次读取的字节数

        // 循环发送和接收数据,直到完成
        while (totalBytesRcvd < argument.length) {
            if (writeBuf.hasRemaining()) {
                clntChan.write(writeBuf); // 将数据写入通道
            }

            // 读取数据到读缓冲区,read() 方法不会阻塞,返回0表示无可用数据
            if ((bytesRcvd = clntChan.read(readBuf)) == -1) {
                throw new SocketException("连接意外关闭"); // 如果返回-1,表示连接意外关闭
            }

            totalBytesRcvd += bytesRcvd;
            System.out.print("."); // 可以执行其他操作,演示在通信等待期间执行其他任务
        }

        // 打印接收到的数据
        System.out.println("Received: " + new String(readBuf.array(), 0, totalBytesRcvd));

        clntChan.close(); // 关闭通道,释放资源
    }
}

NIO Selector

selector.select(TIMEOUT):这一行代码是 Selector 的一个方法调用。它会阻塞等待,直到有通道准备好进行 I/O 操作或者超时。参数 TIMEOUT 表示超时时间,单位是毫秒。如果在超时时间内有通道准备好了,方法会返回已经准备好的通道的数量;如果超时时间内没有通道准备好,方法会返回0。

TCPServerSelector.java

java 复制代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;

public class TCPServerSelector {

    private static final int BUFSIZE = 256; // 缓冲区大小 (字节)
    private static final int TIMEOUT = 3000; // 等待超时时间 (毫秒)

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

        if (args.length < 1) { // 检查参数数量是否正确
            throw new IllegalArgumentException("Parameter(s): <Port> ...");
        }

        // 创建一个 Selector 实例用于多路复用监听套接字和连接
        Selector selector = Selector.open();

        // 为每个端口创建监听套接字通道并注册到 Selector
        for (String arg : args) {
            ServerSocketChannel listnChannel = ServerSocketChannel.open();
            listnChannel.socket().bind(new InetSocketAddress(Integer.parseInt(arg)));
            listnChannel.configureBlocking(false); // 必须是非阻塞的才能注册
            // 注册 Selector,返回的键值不被使用
            listnChannel.register(selector, SelectionKey.OP_ACCEPT);
        }

        // 创建实现协议的处理程序
        TCPProtocol protocol = new EchoSelectorProtocol(BUFSIZE);

        while (true) { // 永久循环,处理可用的 I/O 操作

            // 等待直到某个通道就绪 (或超时)
            if (selector.select(TIMEOUT) == 0) { // 返回就绪通道的数量
            // 阻塞直到有新的连接
                System.out.print(".");
                continue; // 超时时执行其他任务的演示,继续等待
            }

            // 获取已选择的键集合的迭代器
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();

            while (keyIter.hasNext()) {
                SelectionKey key = keyIter.next(); // 获取键
                // ServerSocketChannel 有挂起的连接请求?
                if (key.isAcceptable()) {
                    protocol.handleAccept(key); // 处理连接请求
                }
                // 客户端 SocketChannel 有待处理数据?
                if (key.isReadable()) {
                    protocol.handleRead(key); // 处理读操作
                }
                // 客户端 SocketChannel 可用于写操作且键是有效的 (即通道未关闭)?
                if (key.isValid() && key.isWritable()) {
                    protocol.handleWrite(key); // 处理写操作
                }
                keyIter.remove(); // 从选择键集合中移除该键
            }
        }
    }
}

SelectionKey.OP_CONNECT:连接就绪事件,用于客户端 SocketChannel。

SelectionKey.OP_ACCEPT:接受连接事件,用于 ServerSocketChannel。

SelectionKey.OP_READ:读就绪事件,用于 SocketChannel。

SelectionKey.OP_WRITE:写就绪事件,用于 SocketChannel。

EchoSelectorProtocol.java

java 复制代码
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.ByteBuffer;
import java.io.IOException;

public class EchoSelectorProtocol implements TCPProtocol {

    private int bufSize; // 缓冲区大小

    public EchoSelectorProtocol(int bufSize) {
        this.bufSize = bufSize;
    }

    // 处理接受连接事件
    public void handleAccept(SelectionKey key) throws IOException {
        // 接受客户端连接
        SocketChannel clntChan = ((ServerSocketChannel) key.channel()).accept();
        clntChan.configureBlocking(false); // 必须是非阻塞的才能注册
        // 将新的通道注册到选择器,并关联一个分配的字节缓冲区
        clntChan.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
    }

    // 处理读取数据事件
    public void handleRead(SelectionKey key) throws IOException {
        // 获取客户端通道和关联的缓冲区
        SocketChannel clntChan = (SocketChannel) key.channel();
        ByteBuffer buf = (ByteBuffer) key.attachment();
        long bytesRead = clntChan.read(buf); // 从通道读取数据到缓冲区
        if (bytesRead == -1) { // 对端关闭连接
            clntChan.close();
        } else if (bytesRead > 0) {
            // 说明有数据被读取,标记对读写都感兴趣
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        }
    }

    // 处理写入数据事件
    public void handleWrite(SelectionKey key) throws IOException {
        // 获取关联的缓冲区
        ByteBuffer buf = (ByteBuffer) key.attachment();
        buf.flip(); // 准备缓冲区写入数据
        SocketChannel clntChan = (SocketChannel) key.channel();
        clntChan.write(buf); // 将数据写入通道
        if (!buf.hasRemaining()) { // 缓冲区全部写入
            // 不再关注写事件
            key.interestOps(SelectionKey.OP_READ);
        }
        buf.compact(); // 为读入更多数据腾出空间
    }
}
相关推荐
孟紫瑶3 分钟前
C#语言的游戏引擎
开发语言·后端·golang
雷渊7 分钟前
Redisson如何保证解锁的线程一定是加锁的线程?
java·后端·面试
AronTing7 分钟前
10-Spring Boot 启动性能优化实战
后端·面试
{⌐■_■}21 分钟前
【go】slice的浅拷贝和深拷贝
开发语言·后端·golang
〆、风神1 小时前
Spring Boot 自定义 Redis Starter 开发指南(附动态 TTL 实现)
spring boot·redis·后端
Asthenia04121 小时前
HashMap 扩容机制与 Rehash 细节分析
后端
DataFunTalk1 小时前
不是劝退,但“BI”基础不佳就先“别搞”ChatBI了!
前端·后端
星星电灯猴1 小时前
flutter项目 发布Google Play
后端
用户9704438781162 小时前
按图搜索1688商品(拍立淘)API 返回值说明
javascript·后端·算法
Fly_hao.belief2 小时前
Spring Boot 框架注解:@ConfigurationProperties
java·spring boot·后端