Android 实现录音功能
思路:
通过媒体录制器MediaRecorder实现:MediaRecorder是Android自带的音频和视频录制工具,它通过操纵摄像头和麦克风完成媒体录制,既可录制视频,又可单独录制音频。
MediaRecorder常用方法(录音与录像通用):
- reset:重置录制资源。
- prepare:准备录制。
- start:开始录制。
- stop:结束录制。
- release:释放录制资源。
- setOnErrorListener:设置错误监听器。可监听服务器异常和未知错误的事件。需要实现接口MediaRecorder.OnErrorListener的onError方法。
- setOnInfoListener:设置信息监听器。可监听录制结束事件,包括达到录制时长或达到录制大小。需要实现接口MediaRecorder.OnInfoListener的onInfo方法。
- setMaxDuration:设置可录制的最大时长,单位毫秒。
- setMaxFileSize:设置可录制的最大文件大小,单位字节。
- setOutputFile:设置输出文件的路径。
setOutputFormat:设置媒体输出格式 | |||
OutputFormat类的输出格式 | 格式分类 | 扩展名 | 格式说明 |
AMR_NB | 音频 | .amr | 窄带格式 |
AMR_WB | 音频 | .amr | 宽带格式 |
AAC_ADTS | 音频 | .aac | 高级的音频传输流格式 |
MPEG_4 | 视频 | .mp4 | MPEG4格式 |
THREE_GPP | 视频 | .3gp | 3GP格式 |
音频录制示例,上代码
一.权限添加
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /><uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /><uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /><uses-permission android:name="android.permission.RECORD_AUDIO"/><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
权限区分版本33及以上和以下
动态权限请求:
使用权限请求库,在app的build.gradle添加
// 权限请求implementation 'com.guolindev.permissionx:permissionx:1.7.1'
使用音频抖动动效
// https://github.com/xfans/VoiceWaveView
implementation 'com.github.xfans:VoiceWaveView:1.0.2'
/*** 请求视频、音频、图片权限*/private void requestPermission() {ArrayList<String> requestList = new ArrayList<>();if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {requestList.add(Manifest.permission.READ_MEDIA_IMAGES);requestList.add(Manifest.permission.RECORD_AUDIO);requestList.add(Manifest.permission.READ_MEDIA_VIDEO);} else {requestList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);requestList.add(Manifest.permission.READ_EXTERNAL_STORAGE);requestList.add(Manifest.permission.RECORD_AUDIO);}PermissionX.init(this).permissions(requestList).onExplainRequestReason((scope, deniedList) -> {scope.showRequestReasonDialog(deniedList, UiUtil.getString(R.string.toast_permission_request), UiUtil.getString(R.string.toast_permission_allow), UiUtil.getString(R.string.toast_permission_deny));}).request((allGranted, grantedList, deniedList) -> {if (allGranted) {showVoiceAddDialog();LogUtil.i(TAG, "所有申请的权限都已通过");} else {LogUtil.i(TAG, "您拒绝了如下权限:" + deniedList);}});}/*** 判断是否有存储、音频权限* @return */public boolean hasStoragePermissions() {boolean isStorage, isAudio, isVideo, isImages;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {isImages = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.READ_MEDIA_IMAGES)== PackageManager.PERMISSION_GRANTED;isAudio = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.RECORD_AUDIO)== PackageManager.PERMISSION_GRANTED;isVideo = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.READ_MEDIA_VIDEO)== PackageManager.PERMISSION_GRANTED;return isImages && isAudio && isVideo;} else {isStorage = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.WRITE_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;isAudio = ContextCompat.checkSelfPermission(MyApplication.getInstance(), Manifest.permission.RECORD_AUDIO)== PackageManager.PERMISSION_GRANTED;return isStorage || isAudio;}}
二.录制代码
package com.calendar.master.gp.dialog;import android.app.AlertDialog;
import android.app.Dialog;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.format.DateFormat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;import com.calendar.master.gp.R;
import com.calendar.master.gp.bean.NoteVoice;
import com.calendar.master.gp.databinding.DialogAddVoiceBinding;
import com.calendar.master.gp.databinding.DialogEmojiBinding;
import com.calendar.master.gp.fragment.EmojiFragment;
import com.calendar.master.gp.listener.IVoiceSaveListener;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.oooppqqzzz.base.utils.LogUtil;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;import me.xfans.lib.voicewaveview.VoiceWaveView;/*** 描述:* 作者: shawn* 时间: 2023/4/2716:44*/
public class VoiceAddDialogFragment extends DialogFragment {private static final String TAG = "VoiceAddDialogFragment";private MediaRecorder mMediaRecorder;private String voicePath;private String outputFileName;private File fileVoice;private IVoiceSaveListener iVoiceSaveListener;private DialogAddVoiceBinding mBinding;private int recordingTime = 0; // 记录录音时长(秒)private Handler handler = new Handler(Looper.getMainLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {// 更新UI显示录音时长mBinding.tvVoiceTime.setText(getRecordTime(recordingTime));}};;@Overridepublic void onStart() {super.onStart();setCustomStyle();}@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {if (getDialog() == null) {return super.onCreateView(inflater, container, savedInstanceState);}// 设置宽度为屏宽、靠近屏幕底部。final Window window = getDialog().getWindow();window.setBackgroundDrawableResource(R.color.transparent);window.getDecorView().setPadding(0, 0, 0, 0);WindowManager.LayoutParams wlp = window.getAttributes();wlp.gravity = Gravity.BOTTOM;wlp.width = WindowManager.LayoutParams.MATCH_PARENT;wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;window.setAttributes(wlp);return super.onCreateView(inflater, container, savedInstanceState);}@NonNull@Overridepublic Dialog onCreateDialog(Bundle savedInstanceState) {// 加载布局文件mBinding = DialogAddVoiceBinding.inflate(getLayoutInflater());// 音频抖动控件VoiceWaveView voiceWaveView = mBinding.vwvVoice;voiceWaveView.setDuration(100);voiceWaveView.addHeader(12).addHeader(14).addHeader(26);voiceWaveView.addBody(27).addBody(17).addBody(38).addBody(88).addBody(38).addBody(24).addBody(8).addBody(18).addBody(48).addBody(30).addBody(60).addBody(38);voiceWaveView.addFooter(24).addFooter(14).addFooter(8);mBinding.ivClose.setOnClickListener(v -> {this.dismiss();});mBinding.ivVoiceEdit.setOnClickListener(new View.OnClickListener() {boolean isInput = true;@Overridepublic void onClick(View v) {if (isInput) {isInput = false;voiceWaveView.start();mBinding.ivVoiceEdit.setBackgroundResource(R.mipmap.icon_voice_edit_stop);getPath();startRecord();} else {isInput = true;voiceWaveView.stop();cancelRecord();// 移除定时任务handler.removeCallbacks(timerRunnable);voiceStop(true);}}});mBinding.ivVoiceCancel.setOnClickListener(v -> {voiceStop(false);fileVoice.delete();recordingTime = 0;mBinding.tvVoiceTime.setText("00:00");mBinding.ivVoiceEdit.setBackgroundResource(R.mipmap.icon_voice_edit_start);});mBinding.ivVoiceSave.setOnClickListener(v -> {NoteVoice noteVoice = new NoteVoice();noteVoice.setUrl(voicePath);noteVoice.setTime(getRecordTime(recordingTime));noteVoice.setFileName(outputFileName);if (iVoiceSaveListener != null) {iVoiceSaveListener.save(noteVoice);}dismiss();});return new AlertDialog.Builder(getActivity()).setView(mBinding.getRoot()).create();}private void showAd(int selectIndex) {}private void voiceStop(boolean isStop) {if (isStop) {mBinding.ivVoiceCancel.setVisibility(View.VISIBLE);mBinding.ivVoiceSave.setVisibility(View.VISIBLE);mBinding.ivVoiceEdit.setVisibility(View.GONE);} else {mBinding.ivVoiceCancel.setVisibility(View.GONE);mBinding.ivVoiceSave.setVisibility(View.GONE);mBinding.ivVoiceEdit.setVisibility(View.VISIBLE);}}private void setCustomStyle() {// 设置主题,这里只能通过xml方式设置主题,不能通过Java代码处理,因为这是getWindow还是null,// 而且window的几乎所有属性,都可以通过xml设置setStyle(STYLE_NORMAL, R.style.AddDialogTheme);}/*** 录制前创建一个空文件并获取路径*/private void getPath() {File appDir = new File(Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_MUSIC, "NoteToVoice");if (!appDir.exists()) {appDir.mkdirs();}outputFileName = System.currentTimeMillis() + ".amr";fileVoice = new File(appDir, outputFileName);if (!fileVoice.exists()) {try {fileVoice.createNewFile();voicePath = fileVoice.getPath();LogUtil.i("shawn", "音频保存路径:" + voicePath);} catch (IOException e) {e.printStackTrace();}}}/*** 开始录音*/private void startRecord() {// 充值录音时长recordingTime = 0;//开始录音操作mMediaRecorder = new MediaRecorder(); // 创建一个媒体录制器mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {@Overridepublic void onError(MediaRecorder mr, int what, int extra) {if (mr != null) {mr.reset(); // 重置媒体录制器}}}); // 设置媒体录制器的错误监听器mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {@Overridepublic void onInfo(MediaRecorder mr, int what, int extra) {// 录制达到最大时长,或者达到文件大小限制,都停止录制if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED|| what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {cancelRecord();}}}); // 设置媒体录制器的信息监听器mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频源为麦克风mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); // 设置媒体的输出格式mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 设置媒体的音频编码器// mMediaRecorder.setAudioSamplingRate(8); // 设置媒体的音频采样率。可选// mMediaRecorder.setAudioChannels(2); // 设置媒体的音频声道数。可选// mMediaRecorder.setAudioEncodingBitRate(1024); // 设置音频每秒录制的字节数。可选mMediaRecorder.setMaxDuration(10 * 1000); // 设置媒体的最大录制时长// mMediaRecorder.setMaxFileSize(1024*1024*10); // 设置媒体的最大文件大小// setMaxFileSize与setMaxDuration设置其一即可mMediaRecorder.setOutputFile(voicePath); // 设置媒体文件的保存路径try {mMediaRecorder.prepare(); // 媒体录制器准备就绪mMediaRecorder.start(); // 媒体录制器开始录制startRecordingTimer();} catch (Exception e) {LogUtil.i("shawn", "录音出错 = " + e.getMessage());e.printStackTrace();}}// 取消录制操作private void cancelRecord() {if (mMediaRecorder != null) {mMediaRecorder.setOnErrorListener(null); // 错误监听器置空mMediaRecorder.setPreviewDisplay(null); // 预览界面置空try {LogUtil.i("shawn", "结束录音");mMediaRecorder.stop(); // 媒体录制器停止录制} catch (Exception e) {LogUtil.i("shawn", "结束录音出错 = " + e.getMessage());e.printStackTrace();}mMediaRecorder.release(); // 媒体录制器释放资源mMediaRecorder = null;}}@Overridepublic void onDestroy() {super.onDestroy();// 移除定时任务handler.removeCallbacks(timerRunnable);}public void setVoiceSaveListener(IVoiceSaveListener iVoiceSaveListener) {this.iVoiceSaveListener = iVoiceSaveListener;}private void startRecordingTimer() {handler.postDelayed(timerRunnable, 1000); // 每隔一秒执行一次}private Runnable timerRunnable = new Runnable() {@Overridepublic void run() {recordingTime++;handler.sendEmptyMessage(0); // 通知UI更新startRecordingTimer(); // 继续下一次定时}};private String getRecordTime(int recordingTime) {StringBuilder stringBuilder = new StringBuilder();if (recordingTime < 10) {stringBuilder.append("00:0").append(recordingTime);} else if (recordingTime < 60) {stringBuilder.append("00:").append(recordingTime);} else {int minutes = recordingTime / 60;int seconds = recordingTime % 60;if (minutes < 10) {stringBuilder.append("0").append(minutes).append(":");} else {stringBuilder.append(minutes).append(":");}if (seconds < 10) {stringBuilder.append("0").append(seconds);} else {stringBuilder.append(seconds);}}return stringBuilder.toString();}
}
设置dialog样式,在style.xml
<!-- 这里的parent必须是Theme.AppCompat.Dialog --><style name="AddDialogTheme" parent="Theme.AppCompat.Dialog"><!-- 这两个属性对于一个常规的Dialog,一般必须设置的--><!-- 这两个属性按照下面的值设置之后,确保了弹窗的实际显示效果,跟你在layout文件中的定义效果是一样的 --><item name="android:windowIsFloating">false</item>
<!-- <item name="android:windowBackground">@drawable/radius_add_dialog_bg</item>--></style>
相关文章:
Android 实现录音功能
思路: 通过媒体录制器MediaRecorder实现:MediaRecorder是Android自带的音频和视频录制工具,它通过操纵摄像头和麦克风完成媒体录制,既可录制视频,又可单独录制音频。 MediaRecorder常用方法(录音与录像通用)…...

drawio导出矢量图
1.选中要导出的图 2.导出为pdf 3.用adobe打开pdf,另存为eps...
关于angular router-outlet
关于angular router-outlet Angular是一个现代化的前端框架,它提供了很多强大的工具来帮助我们开发出高效的Web应用。其中一个最常用的功能是路由(routing)系统,它允许我们在不同的URL之间导航并加载不同的组件。而<router-ou…...
设计模式详解-桥接模式
类型:结构型模式 实现原理:将抽象类和实现类分离,使其独立,然后使用接口再将二者连接起来。 意图:将抽象部分与实现部分分离,使它们都可以独立的变化。 主要解决:类变化频繁时用继承可能会出…...
设计模式—— 单一职责原则
文章目录 设计模式的目的设计模式原则单一职责原则基本介绍应用实例单一职责原则注意事项和细节 设计模式的目的 1,代码重用性(即:相同功能的代码,不用多次编写) 2,可读性(即:编程…...
嵌入式系统中如何选择RTC电池?
RTC(Real Time Clock)是一种用于提供系统时间的独立定时器,它可以在系统断电或低功耗模式下继续运行,只需要一个后备电池作为供电源。在嵌入式系统中,选择合适的RTC电池时非常关键的,它会影响系统时间的准确…...
56 | 国内游戏直播竞品分析
国内游戏直播竞品分析 一、需求分析 当前直播用户群可分为两大类: 主播观众用户需求: 1.主播: 作为直播内容的创造者,主播表现方式和内容很大程度上决定了观众的需求, 其中主播主要只有三点需求: (一) 通过某一手段(如游戏技术、唱歌技巧)获取他人关注,满足虚荣心…...

STM32 CubeMX (第一步Freertos任务管理:创建、删除、挂起、恢复)
STM32 CubeMX Freertos STM32 CubeMX (Freertos任务:创建、删除、挂起、恢复) STM32 CubeMX Freertos前言一、STM32 CubeMX 配置时钟树配置HAL时基选择TIM1(不要选择滴答定时器;滴答定时器留给OS系统做时基)…...

0101读写分离测试-jdbc-shardingsphere-中间件
文章目录 1 前言2、创建SpringBoot程序2.1、创建项目2.2、添加依赖2.3、生成实体类、service与Mapper1.5、配置读写分离 2、测试2.1、读写分离测试2.2、事务测试2.3、负载均衡测试 结语 1 前言 shardingshpere-jdbc定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的…...
sqlite3将词典导入数据库
使用sqlite3代码实现将词典导入数据库中 #include <head.h> #include <sqlite3.h> #include <strings.h> #include <unistd.h> int main(int argc, const char *argv[]) {sqlite3 *db NULL;if(sqlite3_open("./dict.db",&db) ! SQLITE…...

浏览器 - 事件循环机制详解
目录 1,浏览器进程模型进程线程浏览器的进程和线程1,浏览器进程2,网络进程3,渲染进程 2,渲染主线程事件循环异步同步 JS 为什么会阻塞渲染任务优先级 3,常见面试题1,如何理解 js 的异步2&#x…...
析构函数中不应该抛出异常(摘录)
析构函数中抛出异常时概括性总结 从语法上面讲,析构函数抛出异常是可以的,C并没有禁止析构函数引发异常,但是C不推荐这一做法,从析构函数中抛出异常是及其危险的。 如果析构函数抛出异常,则异常点之后的程序不会执行&a…...

Windows定时任务计划无法显示任务程序界面的问题解决
笔者这两天写了一个python脚本程序,用来自动从公司的主数据系统获取数据,并按格式编制成excel。脚本程序编写一切顺利,运行结果很是完美,笔者很是舒心。但在最后一步,用上班的电脑每天早上定时运行它时,出了…...

【Azure API 管理】APIM如何实现对部分固定IP进行访问次数限制呢?如60秒10次请求
问题描述 使用Azure API Management, 想对一些固定的IP地址进行访问次数的限制,如被限制的IP地址一分钟可以访问10次,而不被限制的IP地址则可以无限访问? ChatGPT 解答 最近ChatGPT爆火,所以也把这个问题让ChatGPT来解答&#x…...

Python学习笔记_进阶篇(二)_django知识(一)
本章简介: Django 简介Django 基本配置Django urlDjango viewDjango 模板语言Django Form Django 简介 Django是一个开放源代码的Web应用框架,由Python写成。采用了MVC的软件设计模式,即模型M,视图V和控制器C。它最初是被开发来…...

【hive】hive中row_number() rank() dense_rank()的用法
hive中row_number() rank() dense_rank()的用法 一、函数说明 主要是配合over()窗口函数来使用的,通过over(partition by order by )来反映统计值的记录。 rank() over()是跳跃排序,有两个第二名时接下来就是第四名(同样是在各个分组内)dense_rank() …...

【云原生】【k8s】Kubernetes+EFK构建日志分析安装部署
目录 EFK安装部署 一、环境准备(所有主机) 1、主机初始化配置 2、配置主机名并绑定hosts,不同主机名称不同 3、主机配置初始化 4、部署docker环境 二、部署kubernetes集群 1、组件介绍 2、配置阿里云yum源 3、安装kubelet kubeadm …...

计算实数数组中所有元素的绝对值 numpy.fabs()
【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 计算实数数组中所有元素的绝对值 numpy.fabs() [太阳]选择题 请问关于以下代码表述错误的是? iimport numpy as np a np.array([-1,-3]) b np.array([-1,34j]) print("【显…...
深入浅出Pytorch函数——torch.nn.init.orthogonal_
分类目录:《深入浅出Pytorch函数》总目录 相关文章: 深入浅出Pytorch函数——torch.nn.init.calculate_gain 深入浅出Pytorch函数——torch.nn.init.uniform_ 深入浅出Pytorch函数——torch.nn.init.normal_ 深入浅出Pytorch函数——torch.nn.init.c…...
ORACLE中UNION、UNION ALL、MINUS、INTERSECT学习
1、UNION和UNION ALL的使用与区别 如果我们需要将两个select语句的结果作为一个整体显示出来,我们就需要用到union或者union all关键字。union的作用是将多个结果合并在一起显示出来。 union和union all的区别是union会自动压缩多个结果集合中的重复结果ÿ…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
TCP/IP 网络编程 | 服务端 客户端的封装
设计模式 文章目录 设计模式一、socket.h 接口(interface)二、socket.cpp 实现(implementation)三、server.cpp 使用封装(main 函数)四、client.cpp 使用封装(main 函数)五、退出方法…...
Windows 下端口占用排查与释放全攻略
Windows 下端口占用排查与释放全攻略 在开发和运维过程中,经常会遇到端口被占用的问题(如 8080、3306 等常用端口)。本文将详细介绍如何通过命令行和图形化界面快速定位并释放被占用的端口,帮助你高效解决此类问题。 一、准…...
Netty自定义协议解析
目录 自定义协议设计 实现消息解码器 实现消息编码器 自定义消息对象 配置ChannelPipeline Netty提供了强大的编解码器抽象基类,这些基类能够帮助开发者快速实现自定义协议的解析。 自定义协议设计 在实现自定义协议解析之前,需要明确协议的具体格式。例如,一个简单的…...