메시징은 다양한 웹사이트에서 널리 사용되며, 이 기능은 웹사이트에도 필수적입니다. 이 기사에서는 주로 PHP 메시지 전달에 Redis를 적용하는 방법을 소개합니다.
콘텐츠 읽기
1. 요약
2. 구현 방법
3. 4. 더 많은 메시지
1. 요약메시징의 응용은 다양한 웹사이트에서 널리 사용되고 있으며, 이 기능은 웹사이트에도 필수적입니다. 일반적인 메시징 애플리케이션에는 Sina Weibo의 @me, 프롬프트, 비공개 메시지, Weibo에서 공유된 비공개 메시지 및 전송된 Zhihu 메시지, Zhihu 팀 메시지 등의 새로운 메시지가 포함됩니다.
2. 구현 방법
메시지 전달은 둘 이상의 클라이언트가 서로 메시지를 보내고 받는 것을 의미합니다.
이를 달성하는 방법에는 일반적으로 두 가지가 있습니다.
첫 번째는 메시지 푸시입니다. Redis에는 채널에 메시지를 푸시하고 채널을 구독하기 위해 게시하는 메커니즘이 내장되어 있습니다. 이 방법의 한 가지 단점은 수신자가 항상 온라인 상태인지 확인해야 한다는 것입니다(즉, 현재 프로그램을 중지할 수 없으며 모니터링 상태를 유지합니다. 클라이언트 연결이 끊어지면 클라이언트는 정보를 잃게 됩니다)
두 번째는 Pick Pull 메시지입니다. 소위 메시지 풀링은 클라이언트가 서버에 저장된 데이터를 독립적으로 얻는 것을 의미합니다. Redis는 내부적으로 메시지 풀링 메커니즘을 구현하지 않습니다. 따라서 이 기능을 구현하려면 수동으로 코드를 직접 작성해야 합니다.
여기에서는 메시징을 일대일 메시징, 다대다 메시징(그룹 메시징)으로 더 세분화합니다. [참고: 두 클래스의 코드가 상대적으로 크기 때문에 접혀 있습니다.]
3. 일대일 메시징예 1: 일대일 메시지 전송 및 획득
모듈 요구 사항: 1. 새 메시지를 보낸 사람 수 확인 2 정보에는 메시지의 보낸 사람, 시간 및 내용이 포함됩니다.
3. 이전 메시지를 가져오는 기능
4. 메시지는 7일 동안 보관할 수 있으며, 만료되면 수동적으로 삭제됩니다
Redis 구현 아이디어:
1 새 메시지와 이전 메시지는 각각 두 개의 연결 목록에 저장됩니다2. 배열 형식으로 저장되며 보내는 사람, 타임스탬프, 정보 콘텐츠가 포함됩니다 3. Redis 연결 목록에 푸시하기 전에 데이터를 json 유형으로 변환한 다음 저장해야 합니다
4. , rpoplpush를 사용하여 이를 구현해야 합니다. 메시지는 이전 메시지 연결 목록에 푸시됩니다.
5. 이전 메시지를 꺼낼 때 시간이 초과되면 이전 메시지의 시간을 비교해야 합니다. 이후의 모든 데이터는 직접적으로(시간에 따라 하나씩 Linked List에 푸시되기 때문에) 시간순으로 정렬됩니다)
데이터 저장 구조도:
PHP 구현 코드:#SinglePullMessage.class.php<?php
#单接接收者接收消息
class SinglePullMessage
{
private $redis=''; #存储redis对象
/**
* @desc 构造函数
*
* @param $host string | redis主机
* @param $port int | 端口
*/
public function __construct($host,$port=6379)
{
$this->redis=new Redis();
$this->redis->connect($host,$port);
}
/**
* @desc 发送消息(一个人)
*
* @param $toUser string | 接收人
* @param $messageArr array | 发送的消息数组,包含sender、message、time
*
* @return bool
*/
public function sendSingle($toUser,$messageArr)
{
$json_message=json_encode($messageArr); #编码成json数据
return $this->redis->lpush($toUser,$json_message); #将数据推入链表
}
/**
* @desc 用户获取新消息
*
* @param $user string | 用户名
*
* @return array 返回数组,包含多少个用户发来新消息,以及具体消息
*/
public function getNewMessage($user)
{
#接收新信息数据,并且将数据推入旧信息数据链表中,并且在原链表中删除
$messageArr=array();
while($json_message=$this->redis->rpoplpush($user, 'preMessage_'.$user))
{
$temp=json_decode($json_message); #将json数据变成对象
$messageArr[$temp->sender][]=$temp; #转换成数组信息
}
if($messageArr)
{
$arr['count']=count($messageArr); #统计有多少个用户发来信息
$arr['messageArr']=$messageArr;
return $arr;
}
return false;
}
public function getPreMessage($user)
{
##取出旧消息
$messageArr=array();
$json_pre=$this->redis->lrange('preMessage_'.$user, 0, -1); #一次性将全部旧消息取出来
foreach ($json_pre as $k => $v)
{
$temp=json_decode($v); #json反编码
$timeout=$temp->time+60*60*24*7; #数据过期时间 七天过期
if($timeout<time()) #判断数据是否过期
{
if($k==0) #若是最迟插入的数据都过期了,则将所有数据删除
{
$this->redis->del('preMessage_'.$user);
break;
}
$this->redis->ltrim('preMessage_'.$user, 0, $k); #若检测出有过期的,则将比它之前插入的所有数据删除
break;
}
$messageArr[$temp->sender][]=$temp;
}
return $messageArr;
}
/**
* @desc 消息处理,没什么特别的作用。在这里这是用来处理数组信息,然后将其输出。
*
* @param $arr array | 需要处理的信息数组
*
* @return 返回打印输出
*/
public function dealArr($arr)
{
foreach ($arr as $k => $v)
{
foreach ($v as $k1 => $v2)
{
echo '发送人:'.$v2->sender.' 发送时间:'.date('Y-m-d h:i:s',$v2->time).'<br/>';
echo '消息内容:'.$v2->message.'<br/>';
}
echo "<hr/>";
}
}
}
테스트:
1 메시지 보내기
#build test1.phpinclude './SinglePullMessage.class.php';
$object=new SinglePullMessage('192.168.95.11');
#发送消息
$sender='boss'; #发送者
$to='jane'; #接收者
$message='How are you'; #信息
$time=time();
$arr=array('sender'=>$sender,'message'=>$message,'time'=>$time);
echo $object->sendSingle($to,$arr);
2 새로 구매하세요. 메시지
#build test2.phpinclude './SinglePullMessage.class.php';
$object=new SinglePullMessage('192.168.95.11');
#获取新消息
$arr=$object->getNewMessage('jane');
if($arr)
{
echo $arr['count']."个联系人发来新消息<br/><hr/>";
$object->dealArr($arr['messageArr']);
}
else
echo "无新消息";
방문 결과:
3. 오래된 메시지 가져오기#Build test3.php include './SinglePullMessage.class.php';
$object=new SinglePullMessage('192.168.95.11');
#获取旧消息
$arr=$object->getPreMessage('jane');
if($arr)
{
$object->dealArr($arr);
}
else
echo "无旧数据";
4. 다대다 메시징
예 2: 다대다 메시지 보내기 및 받기(예: 그룹)
모듈 요구 사항:1. 사용자는 스스로 그룹을 만들고 그룹이 될 수 있습니다. 리더2. 그룹 리더는 사람들을 그룹 구성원으로 데려오고 사람들을 추방할 수 있습니다.
3. 사용자는 그룹에서 직접 탈퇴할 수 있습니다.
4. 각 구성원은 메시지를 가져올 수 있습니다
5. 그룹의 메시지는 5,000
6개입니다. 회원은 새 메시지를 가져올 수 있으며 새 메시지가 몇 개인지 확인할 수 있습니다.
7. 회원은 페이지에서 이전에 읽은 메시지를 받을 수 있습니다
. . . . . 연습이 필요하거나 연습하고 싶은 학생들은 음소거, 익명 메시지 보내기, 파일 보내기 등과 같은 다른 기능을 추가할 수 있습니다.
Redis 구현 아이디어:
1、群组的消息以及群组的成员组成采用有序集合进行存储。群组消息有序集合的member存储用户发送的json数据消息,score存储唯一值,将采用原子操作incr获取string中的自增长值进行存储;群组成员有序集合的member存储user,score存储非零数字(在这里这个score意义不大,我的例子代码中使用数字1为群主的score,其他的存储为2。当然这使用这个数据还可以扩展别的功能,例如群组中成员等级)可参考下面数据存储结构简图。
2、用户所加入的群组也是采用有序集合进行存储。其中,member存储群组ID,score存储用户已经获取该群组的最大消息分值(对应群组消息的score值)
3、用户创建群组的时候,通过原子操作incr从而获取一个唯一ID
4、用户在群中发送消息时,也是通过原子操作incr获取一个唯一自增长有序ID
5、在执行incr时,为防止并发导致竞争关系,因此需要进行加锁操作【redis详细锁的讲解可以参考:Redis构建分布式锁http://www.jb51.net/article/109704.htm】
6、创建群组方法简要思路,任何一个用户都可以创建群组聊天,在创建的同时,可以选择时是否添加群组成员(参数通过数组的形式)。创建过程将会为这个群组建立一个群组成员有序集合(群组信息有序集合暂时不创建),接着将群主添加进去,再将群ID添加用户所参加的群组有序集合中。
数据存储结构图:
PHP的代码实现:
#ManyPullMessage.class.php
<?php class ManyPullMessage { private $redis=''; #存储redis对象 /** * @desc 构造函数 * * @param $host string | redis主机 * @param $port int | 端口 */ public function __construct($host,$port=6379) { $this->redis=new Redis(); $this->redis->connect($host,$port); } /** * @desc 用于创建群组的方法,在创建的同时还可以拉人进群组 * * @param $user string | 用户名,创建群组的主人 * @param $addUser array | 其他用户构成的数组 * * @param $lockName string | 锁的名字,用于获取群组ID的时候用 * @return int 返回群组ID */ public function createGroupChat($user, $addUser=array(), $lockName='chatIdLock') { $identifier=$this->getLock($lockName); #获取锁 if($identifier) { $id=$this->redis->incr('groupChatID'); #获取群组ID $this->releaseLock($lockName,$identifier); #释放锁 } else return false; $messageCount=$this->redis->set('countMessage_'.$id, 0); #初始化这个群组消息计数器 #开启非事务型流水线,一次性将所有redis命令传给redis,减少与redis的连接 $pipe=$this->redis->pipeline(); $this->redis->zadd('groupChat_'.$id, 1, $user); #创建群组成员有序集合,并添加群主 #将这个群组添加到user所参加的群组有序集合中 $this->redis->zadd('hasGroupChat_'.$user, 0, $id); foreach ($addUser as $v) #创建群组的同时需要添加的用户成员 { $this->redis->zadd('groupChat_'.$id, 2, $v); $this->redis->zadd('hasGroupChat_'.$v, 0, $id); } $pipe->exec(); return $id; #返回群组ID } /** * @desc 群主主动拉人进群 * * @param $user string | 群主名 * @param $groupChatID int | 群组ID * @param $addMembers array | 需要拉进群的用户 * * @return bool */ public function addMembers($user, $groupChatID, $addMembers=array()) { $groupMasterScore=$this->redis->zscore('groupChat_'.$groupChatID, $user); #将groupChatName的群主取出来 if($groupMasterScore==1) #判断user是否是群主 { $pipe=$this->redis->pipeline(); #开启非事务流水线 foreach ($addMembers as $v) { $this->redis->zadd('groupChat_'.$groupChatID, 2, $v); #添加进群 $this->redis->zadd('hasGroupChat_'.$v, 0, $groupChatID); #添加群名到用户的有序集合中 } $pipe->exec(); return true; } return false; } /** * @desc 群主删除成员 * * @param $user string | 群主名 * @param $groupChatID int | 群组ID * @param $delMembers array | 需要删除的成员名字 * * @return bool */ public function delMembers($user, $groupChatID, $delMembers=array()) { $groupMasterScore=$this->redis->zscore('groupChat_'.$groupChatID, $user); if($groupMasterScore==1) #判断user是否是群主 { $pipe=$this->redis->pipeline(); #开启非事务流水线 foreach ($delMembers as $v) { $this->redis->zrem('groupChat_'.$groupChatID, $v); $this->redis->zrem('hasGroupChat_'.$v, $groupChatID); } $pipe->exec(); return true; } return false; } /** * @desc 退出群组 * * @param $user string | 用户名 * @param $groupChatID int | 群组名 */ public function quitGroupChat($user, $groupChatID) { $this->redis->zrem('groupChat_'.$groupChatID, $user); $this->redis->zrem('hasGroupChat_'.$user, $groupChatID); return true; } /** * @desc 发送消息 * * @param $user string | 用户名 * @param $groupChatID int | 群组ID * @param $messageArr array | 包含发送消息的数组 * @param $preLockName string | 群消息锁前缀,群消息锁全名为countLock_群ID * * @return bool */ public function sendMessage($user, $groupChatID, $messageArr, $preLockName='countLock_') { $memberScore=$this->redis->zscore('groupChat_'.$groupChatID, $user); #成员score if($memberScore) { $identifier=$this->getLock($preLockName.$groupChatID); #获取锁 if($identifier) #判断获取锁是否成功 { $messageCount=$this->redis->incr('countMessage_'.$groupChatID); $this->releaseLock($preLockName.$groupChatID,$identifier); #释放锁 } else return false; $json_message=json_encode($messageArr); $this->redis->zadd('groupChatMessage_'.$groupChatID, $messageCount, $json_message); $count=$this->redis->zcard('groupChatMessage_'.$groupChatID); #查看信息量大小 if($count>5000) #判断数据量有没有达到5000条 { #数据量超5000,则需要清除旧数据 $start=5000-$count; $this->redis->zremrangebyrank('groupChatMessage_'.$groupChatID, $start, $count); } return true; } return false; } /** * @desc 获取新信息 * * @param $user string | 用户名 * * @return 成功则放回json数据数组,无新信息返回false */ public function getNewMessage($user) { $arrID=$this->redis->zrange('hasGroupChat_'.$user, 0, -1, 'withscores'); #获取用户拥有的群组ID $json_message=array(); #初始化 foreach ($arrID as $k => $v) #遍历循环所有群组,查看是否有新消息 { $messageCount=$this->redis->get('countMessage_'.$k); #群组最大信息分值数 if($messageCount>$v) #判断用户是否存在未读新消息 { $json_message[$k]['message']=$this->redis->zrangebyscore('groupChatMessage_'.$k, $v+1, $messageCount); $json_message[$k]['count']=count($json_message[$k]['message']); #统计新消息数量 $this->redis->zadd('hasGroupChat_'.$user, $messageCount, $k); #更新已获取消息 } } if($json_message) return $json_message; return false; } /** * @desc 分页获取群组信息 * * @param $user string | 用户名 * @param $groupChatID int | 群组ID * @param $page int | 第几页 * @param $size int | 每页多少条数据 * * @return 成功返回json数据,失败返回false */ public function getPartMessage($user, $groupChatID, $page=1, $size=10) { $start=$page*$size-$size; #开始截取数据位置 $stop=$page*$size-1; #结束截取数据位置 $json_message=$this->redis->zrevrange('groupChatMessage_'.$groupChatID, $start, $stop); if($json_message) return $json_message; return false; } /** * @desc 加锁方法 * * @param $lockName string | 锁的名字 * @param $timeout int | 锁的过期时间 * * @return 成功返回identifier/失败返回false */ public function getLock($lockName, $timeout=2) { $identifier=uniqid(); #获取唯一标识符 $timeout=ceil($timeout); #确保是整数 $end=time()+$timeout; while(time()<$end) #循环获取锁 { /* #这里的set操作可以等同于下面那个if操作,并且可以减少一次与redis通讯 if($this->redis->set($lockName, $identifier array('nx', 'ex'=>$timeout))) return $identifier; */ if($this->redis->setnx($lockName, $identifier)) #查看$lockName是否被上锁 { $this->redis->expire($lockName, $timeout); #为$lockName设置过期时间 return $identifier; #返回一维标识符 } elseif ($this->redis->ttl($lockName)===-1) { $this->redis->expire($lockName, $timeout); #检测是否有设置过期时间,没有则加上 } usleep(0.001); #停止0.001ms } return false; } /** * @desc 释放锁 * * @param $lockName string | 锁名 * @param $identifier string | 锁的唯一值 * * @param bool */ public function releaseLock($lockName,$identifier) { if($this->redis->get($lockName)==$identifier) #判断是锁有没有被其他客户端修改 { $this->redis->multi(); $this->redis->del($lockName); #释放锁 $this->redis->exec(); return true; } else { return false; #其他客户端修改了锁,不能删除别人的锁 } } } ?>
测试:
1、建立createGroupChat.php(测试创建群组功能)
执行代码并创建568、569群组(群主为jack)
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); #创建群组 $user='jack'; $arr=array('jane1','jane2'); $a=$object->createGroupChat($user,$arr); echo "<pre class="brush:php;toolbar:false">"; print_r($a); echo "";die;
2、建立addMembers.php(测试添加成员功能)
执行代码并添加新成员
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); $b=$object->addMembers('jack','568',array('jane1','jane2','jane3','jane4')); echo "<pre class="brush:php;toolbar:false">"; print_r($b); echo "";die;
3、建立delete.php(测试群主删除成员功能)
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); #群主删除成员 $c=$object->delMembers('jack', '568', array('jane1','jane4')); echo "<pre class="brush:php;toolbar:false">"; print_r($c); echo "";die;
4、建立sendMessage.php(测试发送消息功能)
多执行几遍,568、569都发几条
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); #发送消息 $user='jane2'; $message='go go go'; $groupChatID=568; $arr=array('sender'=>$user, 'message'=>$message, 'time'=>time()); $d=$object->sendMessage($user,$groupChatID,$arr); echo "<pre class="brush:php;toolbar:false">"; print_r($d); echo "";die;
5、建立getNewMessage.php(测试用户获取新消息功能)
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); #用户获取新消息 $e=$object->getNewMessage('jane2'); echo "<pre class="brush:php;toolbar:false">"; print_r($e); echo "";die;
6、建立getPartMessage.php(测试用户获取某个群组部分消息)
(多发送几条消息,用于测试。568中共18条数据)
include './ManyPullMessage.class.php'; $object=new ManyPullMessage('192.168.95.11'); #用户获取某个群组部分消息 $f=$object->getPartMessage('jane2', 568, 1, 10); echo "<pre class="brush:php;toolbar:false">"; print_r($f); echo "";die;
page=1,size=10
page=2,size=10
测试完毕,还需要别的功能可以自己进行修改添加测试。
以上就是本文的全部内容,希望对大家的学习有所帮助。
相关推荐:
PHP를 사용하여 Redis 대기열에서 전자상거래 주문 접수를 자동으로 확인하는 방법
위 내용은 PHP Redis의 애플리케이션 메시징에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

phpsession 실패 이유에는 구성 오류, 쿠키 문제 및 세션 만료가 포함됩니다. 1. 구성 오류 : 올바른 세션을 확인하고 설정합니다. 2. 쿠키 문제 : 쿠키가 올바르게 설정되어 있는지 확인하십시오. 3. 세션 만료 : 세션 시간을 연장하기 위해 세션을 조정합니다 .GC_MAXLIFETIME 값을 조정하십시오.

PHP에서 세션 문제를 디버그하는 방법 : 1. 세션이 올바르게 시작되었는지 확인하십시오. 2. 세션 ID의 전달을 확인하십시오. 3. 세션 데이터의 저장 및 읽기를 확인하십시오. 4. 서버 구성을 확인하십시오. 세션 ID 및 데이터를 출력, 세션 파일 컨텐츠보기 등을 통해 세션 관련 문제를 효과적으로 진단하고 해결할 수 있습니다.

Session_Start ()로 여러 통화를하면 경고 메시지와 가능한 데이터 덮어 쓰기가 발생합니다. 1) PHP는 세션이 시작되었다는 경고를 발행합니다. 2) 세션 데이터의 예상치 못한 덮어 쓰기를 유발할 수 있습니다. 3) Session_status ()를 사용하여 반복 통화를 피하기 위해 세션 상태를 확인하십시오.

SESSION.GC_MAXLIFETIME 및 SESSION.COOKIE_LIFETIME을 설정하여 PHP에서 세션 수명을 구성 할 수 있습니다. 1) SESSION.GC_MAXLIFETIME 서버 측 세션 데이터의 생존 시간을 제어합니다. 2) 세션 .Cookie_Lifetime 클라이언트 쿠키의 수명주기를 제어합니다. 0으로 설정하면 브라우저가 닫히면 쿠키가 만료됩니다.

데이터베이스 스토리지 세션 사용의 주요 장점에는 지속성, 확장 성 및 보안이 포함됩니다. 1. 지속성 : 서버가 다시 시작 되더라도 세션 데이터는 변경되지 않아도됩니다. 2. 확장 성 : 분산 시스템에 적용하여 세션 데이터가 여러 서버간에 동기화되도록합니다. 3. 보안 : 데이터베이스는 민감한 정보를 보호하기 위해 암호화 된 스토리지를 제공합니다.

SessionHandlerInterface 인터페이스를 구현하여 PHP에서 사용자 정의 세션 처리 구현을 수행 할 수 있습니다. 특정 단계에는 다음이 포함됩니다. 1) CustomsessionHandler와 같은 SessionHandlerInterface를 구현하는 클래스 만들기; 2) 인터페이스의 방법 (예 : Open, Close, Read, Write, Despare, GC)의 수명주기 및 세션 데이터의 저장 방법을 정의하기 위해 방법을 다시 작성합니다. 3) PHP 스크립트에 사용자 정의 세션 프로세서를 등록하고 세션을 시작하십시오. 이를 통해 MySQL 및 Redis와 같은 미디어에 데이터를 저장하여 성능, 보안 및 확장 성을 향상시킬 수 있습니다.

SessionId는 웹 애플리케이션에 사용되는 메커니즘으로 사용자 세션 상태를 추적합니다. 1. 사용자와 서버 간의 여러 상호 작용 중에 사용자의 신원 정보를 유지하는 데 사용되는 무작위로 생성 된 문자열입니다. 2. 서버는 쿠키 또는 URL 매개 변수를 통해 클라이언트로 생성하여 보낸다. 3. 생성은 일반적으로 임의의 알고리즘을 사용하여 독창성과 예측 불가능 성을 보장합니다. 4. 실제 개발에서 Redis와 같은 메모리 내 데이터베이스를 사용하여 세션 데이터를 저장하여 성능 및 보안을 향상시킬 수 있습니다.

JWT 또는 쿠키를 사용하여 API와 같은 무국적 환경에서 세션을 관리 할 수 있습니다. 1. JWT는 무국적자 및 확장 성에 적합하지만 빅 데이터와 관련하여 크기가 크다. 2. 쿠키는보다 전통적이고 구현하기 쉽지만 보안을 보장하기 위해주의해서 구성해야합니다.


핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

Eclipse용 SAP NetWeaver 서버 어댑터
Eclipse를 SAP NetWeaver 애플리케이션 서버와 통합합니다.

DVWA
DVWA(Damn Vulnerable Web App)는 매우 취약한 PHP/MySQL 웹 애플리케이션입니다. 주요 목표는 보안 전문가가 법적 환경에서 자신의 기술과 도구를 테스트하고, 웹 개발자가 웹 응용 프로그램 보안 프로세스를 더 잘 이해할 수 있도록 돕고, 교사/학생이 교실 환경 웹 응용 프로그램에서 가르치고 배울 수 있도록 돕는 것입니다. 보안. DVWA의 목표는 다양한 난이도의 간단하고 간단한 인터페이스를 통해 가장 일반적인 웹 취약점 중 일부를 연습하는 것입니다. 이 소프트웨어는

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

VSCode Windows 64비트 다운로드
Microsoft에서 출시한 강력한 무료 IDE 편집기
