1. 原码、反码、补码

  1. 正数的原码、反码、补码都一样
  2. 负数的原码,就是最高位使用符号 1,代表负数,如 8bit 表示:10000010 代表 -2 的原码,反码就是符号为不变,其他位按位取反(0-> 1,1-> 0),得到 11111101,

补码=反码 + 1,得到补码:11111110

8 位来算:2 的补码 + -2 的补码 = 0000 0010 + 1111 1110 = 1 0000 0000,溢出,相当于 0

  1. 计算机中的计算都是使用补码来算的,底层存储的 bit 都是一样的,只是解读方式不一样

A - B = A + (-B), A-B 相当于 加上 -B 的补码,-B = ~B + 1

2. int 类型 转 二进制(字符串表示)

/**
* 方法一,思路:
* 从高位开始,每一位都一道最低位与1 进行 按位与,提取出该位,
* 再使用 StringBuilder 拼接
* @param n
* @return
*/
private static String int2Binary(int n) {
//System.out.println(Integer.toBinaryString(a));
StringBuilder sb = new StringBuilder();
for (int i = 31; i >= 0; i--) {
int bit = (n >>> i) & 1;
sb.append(bit);
// 每 8 bit 分隔一下
// 00000000 00000000 00000000 00000101
if (i % 8 == 0 && i != 0) {
sb.append(" ");
}
}
return sb.toString();
}

/**
* 掩码:面具(mask):0 代表遮住,1 代表露出来。
* 得到 int 类型数字的二进制形式 (String表示)
* `(1 << i) & n`得到的是**掩码**,
* 该位为 1 时返回这个位的权重值(4、8、16…),
* **不是 1**,不能直接 append;要判断`!=0`
* 输出字符`'1'/'0'`。
*
* @param n
* @return
*/
public static String intToBinary(int n) {
StringBuilder sb = new StringBuilder();
for (int i = 31; i >= 0; i--) {
// 00000000 00000000 00000000 00000101
// 00000000 00000000 00000000 00000001
int bit = (1 << i) & n;
if (bit != 0) {
sb.append("1");
} else {
sb.append("0");
}
if (i % 8 == 0 && i != 0) {
sb.append(" ");
}
}
return sb.toString();
}

注:其他类型,比如 long,byte,short转 binary 也一样,只需要各数据类型实际的位数即可

3. long 转字节数组 相互转换

为什么要进行转换?

答:long 在 Java 是 64 位,8 个字节。网络传输,要把一个 64 位 long 数字,拆成 8 个 byte 字节数组,才能放到网络报文里发送。

为什么网络只能一个字节一个字节往外发,不能直接发 64 位 long?

1、硬件底层:网卡、TCP/IP 协议,设计的传输单位就是字节(8bit)

网卡在物理链路上,是以比特 bit 一位一位传的,但是协议层把 8 个 bit 打包成 1 个字节 byte,作为最小处理单元。

2、TCP 是字节流协议

TCP 是面向字节流:它传输的是一串无差别的字节序列。 TCP 只关心:“我要发送 N 个字节的数据”,它完全不懂上层的 Java 数据类型:long、int、double。

TCP 的 socket 发送接口:

//输出流,只能丢byte[]字节数组
outputStream.write(byte[]);

3、还有一个致命问题:大小端(字节序)

就算假设网卡可以直接丢 64 位整数:

  • 有的 CPU 是小端:内存里低字节放前面;
  • 有的 CPU 是大端:内存里高字节放前面。

同一 long 数字,在不同 CPU 内存里,字节摆放顺序是反过来的。 如果直接把内存里的 long 二进制原样丢上网络,小端机器发的数据,大端机器收到解析出来就是完全错误的数字。

所以网络协议强制约定:统一使用大端序(网络字节序)。 我们的longToBytes,就是手动按大端顺序,把 long 拆成字节数组。

4、内存存储 vs 网络传输,两件不一样的事

  1. CPU 内存里的 long:64 位,8 字节,CPU 可以直接读写,受本机 CPU 字节序影响。
  2. 网络上传输:跨机器通信,必须定义一个与 CPU 无关的标准二进制格式,也就是大端字节数组。

直接把内存的 long 扔网络:不同机器解析结果不一样,协议就乱套。 所以必须我们手动做转换:long → 大端 byte []。

实战代码:

 /**
* long 转字节数组
* `8*(7‑i)`:计算右移步数,每次把目标字节挪到最右边。
* `&0xFF`清除符号扩展带来的高位垃圾,拿到真实字节值。
*
* @param num
* @return
*/
public static byte[] longToBytes(long num) {
// 创建长度为 8 的 byte 数组
byte[] bytes = new byte[8];
for (int i = 0; i < 8; i++) {
bytes[i] = (byte) ((num >> (8 * (7 - i)) & 0xFF));
}
return bytes;
}

/**
* 字节数组 转 long
*
* @param bytes
* @return
*/
public static long BytesToLong(byte[] bytes) {
long res = 0L;
for (byte b : bytes) {
res = (res << 8) | (b & 0xFF);
}
return res;
}

练习:int 和 byte[] 之间的相互转换

/**
* int 转字节数组,大端(高位放在前)
*
* @param num
* @return
*/
public static byte[] intToBytes(int num) {
byte[] bytes = new byte[4];
for (int i = 0; i < 4; i++) {
bytes[i] = (byte) ((num >> 8 * (3 - i)) & 0xFF);
}
return bytes;
}

/**
* byte[] 转 int
* @param bytes
* @return
*/
public static int BytesToInt(byte[] bytes) {
int res = 0;
for (byte b : bytes) {
res = (res << 8) | (b & 0xFF);
}
return res;
}

4. socket 实现文件上传,其中使用了 long 转 byte[], int 转byte[]

客户端代码:

package com.apitest;

import java.io.*;
import java.net.Socket;

/**
* @author zhangzhicai
* @version 1.0
* @description: 文件上传
* @date 2026/8/24 13:23
*/
public class FileUpload {
public static void main(String[] args) {
// 指定文件路径
File file = new File("D:\\Pictures\\照片\\IMG_0639.JPG");
// 获取文件名
String fileName = file.getName();
// 获取文件大小,单位:字节
long fileLen = file.length();
// 使用 try-with-resource方法
try (FileInputStream fi = new FileInputStream(file);
BufferedInputStream bi = new BufferedInputStream(fi);
Socket socket = new Socket("localhost", 9999);
OutputStream os = socket.getOutputStream()) {
// 1.文件名
byte[] fileNameBytes = fileName.getBytes();
// 2.发送文件扩展名的字节长度
byte[] nameLenBuf = intToBytes(fileNameBytes.length);
os.write(nameLenBuf);
// 3.发送文件名
os.write(fileNameBytes);
// 3.发文件大小
byte[] bytes = longToBytes(fileLen);
os.write(bytes);
// 4.发文件二进制
int len;
byte[] buf = new byte[1024 * 4];
while ((len = bi.read(buf)) != -1) {
os.write(buf, 0, len);
}
// TCP Socket 的输出流内部也有缓冲区。
//write() 只是把数据写到 JDK 内存缓冲区,不一定立刻发给对面服务端。
//os.flush():强制把缓冲区里所有残留字节,发送到网络,发给对方。
// 不是必须的
// 原因:try‑with‑resources结束,会自动调用bo.close(),close()内部会先执行flush(),把缓冲区全部写到文件。
os.flush();
System.out.println("发送完成!");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* long 转大端 8 字节数组
* @param num
* @return
*/
private static byte[] longToBytes(long num) {
// 创建长度为 8 的 byte 数组
byte[] bytes = new byte[8];
for (int i = 0; i < 8; i++) {
bytes[i] = (byte) ((num >> (8 * (7 - i)) & 0xFF));
}
return bytes;
}

/**
* int转大端4字节数组
*/
private static byte[] intToBytes(int num) {
byte[] bytes = new byte[4];
for (int i = 0; i < 4; i++) {
bytes[i] = (byte) ((num >> (8 * (3 - i))) & 0xFF);
}
return bytes;
}

}

服务端代码:

package com.apitest;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/8/24 13:25
*/
public class FileServer {
public static void main(String[] args) {
// 保存目录,不是完整文件!
File saveDir = new File("E:\\test\\img\\");
// 文件夹不存在就创建
if (!saveDir.exists()) {
saveDir.mkdirs();
}
try (ServerSocket serverSocket = new ServerSocket(9999);) {
System.out.println("文件服务器在运行...");
// 使用 while 循环,保持服务端一直运行
while (true) {
try (Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream()) {
// 1.接收文件名长度,4 字节
byte[] fileNameBytes = is.readNBytes(4);
if (fileNameBytes.length != 4) {
throw new IOException("读取文件长度失败,数据流提前断开,只读到:" + fileNameBytes.length + "字节");
}
// 将读取的4 字节bytes,转为int
int fileNameLen = bytesToInt(fileNameBytes);
// 2.接收文件名
byte[] fileNameBuf = is.readNBytes(fileNameLen);
String fileName = new String(fileNameBuf, 0, fileNameLen);
// 获取文件后缀
String[] strings = fileName.split("\\.");
String suffix = strings[1];
// 3.读取 8 字节,为 文件大小
byte[] lenBuf = is.readNBytes(8);
if (lenBuf.length != 8) {
throw new IOException("读取文件长度失败,数据流提前断开,只读到:" + lenBuf.length + "字节");
}
long fileLen = BytesToLong(lenBuf);
// 4. 读二进制文件
// 使用 UUID 生成上传后文件名
fileName = UUID.randomUUID() + "." + suffix;
fileName = fileName.replace("-", "");
File file = new File(saveDir, fileName);
try (FileOutputStream fo = new FileOutputStream(file);
BufferedOutputStream bo = new BufferedOutputStream(fo);) {
byte[] buf = new byte[1024 * 4];
int len;
long totalLen = 0;
// 判断 totalLen < fileLen,读取的文件大小是否到达文件总大小
// `in.read()` 返回 `-1` 的含义:**Socket 连接对方正常关闭输出流**。
while (totalLen < fileLen && (len = is.read(buf)) != -1) {
bo.write(buf, 0, len);
totalLen += len;
}
// 内部有一块内存缓冲区,write() 并不会立刻把字节写到磁盘文件,先写到内存缓冲区;缓冲区满了,才会自动刷入磁盘。
//bo.flush():强制把缓冲区里残留的数据全部写入磁盘文件,清空缓冲区。
bo.flush();
System.out.println("接收成功!");
System.out.println("文件名:" + fileName);
System.out.println("存储路径:" + file.getAbsolutePath());
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* 字节数组 转 long
*
* @param bytes
* @return
*/
private static long BytesToLong(byte[] bytes) {
long res = 0L;
for (byte b : bytes) {
res = (res << 8) | (b & 0xFF);
}
return res;
}

/**
* 字节数组转 int
* @param bytes
* @return
*/
private static int bytesToInt(byte[] bytes) {
int num = 0;
for (byte b : bytes) {
num = (num << 8) | (b & 0xFF);
}
return num;
}

}