이전 :
어제 나는 블로그에 글을 쓰는 데 시간이 걸린 소켓 채팅 프로그램의 초기 디자인을 녹음했습니다. 이 프로그램의 전반적인 디자인이었습니다. 완전성을 위해 오늘은 서버 측의 설계를 자세히 기록 할 것입니다. 홈페이지는 다음과 같이 소켓 채팅 프로그램의 일반 디자인 다이어그램을 게시합니다.
기능 설명 :
서버에는 두 가지 주요 작업이 있습니다. 하나는 수신 클라이언트의 소켓을 차단하고 응답 처리를 수행하고 다른 하나는 클라이언트의 심장 박동을 감지하는 것입니다. 클라이언트가 일정 시간 동안 하트 비트를 보내지 않으면 클라이언트를 제거하고 서버 소켓을 만들고 두 개의 스레드 풀을 시작 하여이 두 가지를 처리합니다 (NewFixedThreadpool, NewsCheDuledthreadpool). 해당 처리 클래스는 SocketDispatcher 및 SocketSchedule입니다. SocketDispatcher는 다른 소켓 요청에 따라 다른 Sockethandlers에 분배됩니다. SocketWrapper는 소켓에 쉘 래퍼를 추가하고 소켓의 최신 상호 작용 시간을 Socketholder Stores와 함께 현재 서버와 상호 작용하는 소켓 컬렉션을 기록합니다.
특정 구현 :
[Server.java]
서버는 서버 입구입니다. Serversocket은 START () 메소드의 서버 메소드에 의해 시작된 다음 수신 클라이언트의 요청을 차단하고 SocketDispatcher에 배포를 위해 양도됩니다. SocketDispatcher는 NewFixedThread 유형의 스레드 풀에 의해 시작됩니다. 연결 횟수가 최대 데이터를 초과하면 대기열에 의해 처리됩니다. ScheduLeatFixEdrate는 고객의 하트 비트 패키지를 들으려면 SocketSchedule 타이밍 루프를 시작하는 데 사용됩니다. 두 유형 모두 실행 가능한 인터페이스를 구현합니다. 다음은 서버의 코드입니다.
yaolin.chat.server; import java.io.ioexception; import java.net.serversocket; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; import java.util.concurrent.schedecutortervice; java.util.concurrent.timeUnit; import yaolin.chat.common.constantValue; import yaolin.chat.util.loggerutil;/*** @author yaolin*/public class server {private serversocket server; 개인 최종 ExecutorService 풀; public server ()는 ioexception {server = new serversocket (constantValue.server_port); pool = executors.newfixedthreadpool (constantValue.max_pool_size); } public void start () {try {ScheduleDexecutorService Schedule = Executors.NewScheduledThreadPool (1); // 개를 시청합니다. 예외?? Schedule.SchedUeAtfixEdrate (new SocketSchedule (), 10, constantValue.time_out, timeUnit.seconds); while (true) {pool.execute (New SocketDispatcher (Server.Accept ())); loggerutil.info ( "" + new date ()에서 클라이언트 수락); }} catch (ioexception e) {pool.shutdown (); }} public static void main (String [] args) {try {new server (). start (); } catch (ioexception e) {loggerutil.error ( "서버 시작 실패! ->" + e.getMessage (), e); }}}[SocketDispatcher.java]
서버는 서버와 명령 센터의 입구 일뿐입니다. SocketDispatcher는 서버의 명령 센터입니다. 클라이언트의 다른 메시지 유형 요청을 배포하므로 다른 Sockethandlers가 해당 메시지 요청을 처리 할 수 있습니다. 여기서 서버와 클라이언트 간의 메시지 상호 작용은 JSON 데이터를 사용합니다. 모든 메시지 클래스는 Basemessage를 상속 받으므로 수신 된 데이터가 Basemessage 유형으로 변환 된 다음 유형이 판단됩니다. (데이터 유형 모듈은 공통 모듈에 속합니다). 여기에서 메시지 유형이 파일 유형 일 때, 실행 간격을 구성하는 것이 잠을 자면서 파일 핸들러가 메시지 유형을 판단하기 위해 다음 루프를 즉시 입력하지 않고 파일 스트림을 지정된 클라이언트에게 읽고 재판매 할 시간을 가질 수 있다고 언급해야합니다 (디자인은 약간 문제가 될 수 있지만 시간 동안이를 수행 할 수 있습니다). 다음은 SocketDispatcher의 코드입니다.
/** * SocketDispatcher * * @author yaolin */public class socketdispatcher emplements runnable {개인 최종 소켓 소켓; public socketdispatcher (소켓 소켓) {this.socket = 소켓; } @Override public void run () {if (socket! = null) {while (! socket.isclosed ()) {try {inputStream은 = socket.getInputStream (); 문자열 라인 = null; StringBuffer sb = null; if (is.available ()> 0) {bufferedReader bufr = new bufferedReader (new inputStreamReader (is)); sb = new StringBuffer (); while (is.aveailable ()> 0 && (line = bufr.readline ())! = null) {sb.append (line); } loggerutil.trach ( "rece [" + sb.toString () + "]에서" + new date ()); BaseMessage 메시지 = json.parseobject (sb.toString (), BaseMessage.class); switch (message.gettype ()) {case messagetype.alive : handlerfactory.gethandler (messagetype.alive) .handle (socket, sb.tostring ()); 부서지다; case messagetype.chat : handlerfactory.gethandler (messagetype.chat) .handle (socket, sb.tostring ()); 부서지다; case messagetype.file : handlerfactory.gethandler (messagetype.file) .handle (socket, sb.tostring ()); 부서지다; case messagetype.file : handlerfactory.gethandler (messagetype.file) .handle (socket, sb.tostring ()); loggerutil.trach ( "sever : 파일을 받기 위해 일시 중지"); thread.sleep (constantvalue.message_period); 부서지다; case messagetype.login : handlerfactory.gethandler (messagetype.login) .handle (socket, sb.tostring ()); 부서지다; Case Messagetype.logout : Break; case messagetype.register : handlerfactory.gethandler (messagetype.register) .handle (socket, sb.tostring ()); 부서지다; }} else {thread.sleep (constantValue.message_period); }} catch (예외 e) {// 모든 핸들러 예외 loggerutil.error ( "SocketDispatcher error!" + e.getMessage (), e); }}}}}}[SocketSchedule.java]
서버와 직접 관련된 다른 클래스 (구성 요소)는 SocketSchedule입니다. SocketSchedule은 주로 클라이언트와 서버 간의 최신 상호 작용 시간이 시스템 구성에서 허용 된 최대 허용 시간을 초과하는지 여부를 감지 할 책임이 있습니다. 이를 초과하면 클라이언트 소켓이 서버에서 제거됩니다. 그렇지 않으면 클라이언트와 서버 간의 최신 상호 작용 시간이 업데이트됩니다. 다음은 특정 구현입니다.
/** * lastalivetime> time_out 인 경우 Socketholder에서 소켓을 제거하십시오 * @author yaolin */public class socketschedule emplements runnable {@override public void run () {for (String key : socketholder.keyset ()) {SocketWrapper = socketholder.get (key); if (wrapper! = null && wrapper.getLastAlivetime ()! = null) {if (((new date (). gettime ()) - wrapper.getLastAliveTime (). getTime ()) / 1000)> constantValue.time_out) {// TimeOut Socket.Remove (key); }}}}}}[Socketholder.java, socketwrapper.java]
위의 코드에서 SocketSchedule#run ()이 단순한 시간의 판단 일뿐임을 알 수 있습니다. 정말로 의미있는 것은 양말 톨더와 소켓 트래퍼입니다. SocketWrapper는 쉘 래퍼를 소켓에 추가합니다. Socketholder는 현재 유효한 시간 동안 서버와 상호 작용하는 모든 클라이언트를 저장합니다. Socketholder는 클라이언트에 의해 독특하게 식별됩니다 (여기서 사용자 이름). 키로 클라이언트가있는 소켓은 키 값 값으로 저장됩니다. Socketholder#FlushclientStatus ()의 처리 로직은 다른 클라이언트에게 현재 클라이언트의 온라인/오프라인 상태를 알리는 데 사용됩니다. 이 두 클래스의 구체적인 구현은 다음과 같습니다.
/** * Wrap Socket, SocketSchedule LastAliveTime> Time_out * @Author Yaolin */public class socketwrapper {개인 소켓 소켓; 개인 날짜 마지막 날짜; // 전체 생성기 공개 소켓 wrapper (소켓 소켓, 날짜 마지막 날짜) {this.socket = socket; this.lastalivetime = lastalivetime; } public socket getSocket () {리턴 소켓; } public void setSocket (소켓 소켓) {this.socket = 소켓; } 공개 날짜 getLastAliveTime () {return lastalivetime; } public void setlastaliveTime (날짜 마지막 날짜) {this.lastaliveTime = lastalivetime; }} /** * SOCKETHOLDER * @Author yaolin */public class socketholder {private static concurrentMap <string, socketWrapper> listSocketWrap = new ConcurrenTashMap <String, SocketWrapper> (); public static set <string> keyset () {return listsocketwrap.keyset (); } public static socketwrapper get (string key) {return listsocketwrap.get (key); } public static void put (문자열 키, socketWrapper 값) {listSocketWrap.put (키, 값); FlushClientStatus (key, true); } public static socketwrapper 제거 (문자열 키) {flushclientStatus (키, 거짓); return listsocketwrap.remove (키); } public static void void clear () {listSocketWrap.Clear (); } /** * <fre> 컨텐츠 : {username : "", flag : false} < /pre> * @param flag true : put, false : 제거; */ private static void flushclientStatus (문자열 키, 부울 플래그) {clientNotifyDto dto = new ClientNotifyDto (플래그, 키); returnMessage rm = new returnMessage (). setkey (key.notify) .setSuccess (true) .setContent (dto); rm.setfrom (constantValue.server_name); for (string tokey : listsocketwrap.keyset ()) {if (! tokey.equals (key)) {// self rm.setto (tokey)로 보내지 않습니다. socketwrapper wrap = listsocketwrap.get (tokey); if (wrap! = null) {sendHelper.Send (wrap.getSocket (), rm); }}}}}}[SOCKETHANDLER.java, handlerFactory.java, elsehandlerimpl.java]
SocketDispatcher를 사용하면 다른 Sockethandler가 해당 메시지 요청을 처리 할 수 있습니다. Sockethandler의 디자인은 실제로 간단한 공장 구성 요소 세트입니다 (ReturnHandler는 SendHelper에 의해 일시적으로 전송되지만 당분간 사용되지 않습니다. @Deprecated되었으며 여전히 여기에 제공됩니다). 전체 클래스 다이어그램은 다음과 같습니다.
이 섹션의 코드는 다음과 같습니다. 공간을 줄이기 위해 핸들러가 구현 한 모든 코드가 수집됩니다.
/** * SOCKETHANDLER * @AUTHOR YAOLIN */PUBLIC 인터페이스 SOCKETHANDLER {/** * 핸들 클라이언트 소켓 */공개 객체 핸들 (소켓 클라이언트, 객체 데이터);} /** * SOCKETHANDLERFACTORY * @AUTHOR YAOLIN */public class handlerFactory {// 인스턴스를 생성 할 수 없습니다 프리즈 핸들러 플렉스 () {} public static sockethandler gethandler (int type) {switch (type) {case messagetype.Alive : // 일반적으로 return new aliveHandler (); Case Messagetype.chat : 새로운 Chathandler ()를 반환합니다. case messagetype.login : return new loginHandler (); // case messagetype.return : // new returnHandler (); case messagetype.logout : return new logouthandler (); case messagetype.register : return new RegisterHandler (); case messagetype.file : return new FileHandler (); } return null; // nullpointexception}} /** * AliveSocketHandler * @author yaolin */public class alivehandler는 foodethandler {/** * @return null */@override public 객체 핸들 (소켓 클라이언트, 객체 데이터) {if (data! = null) {basemessage 메시지 = json.parseobject (data.tostring (), class); if (stringUtil.isnotempty (message.getfrom ())) {socketWrapper wrapper = socketholder.get (message.getfrom ()); if (wrapper! = null) {wrapper.setLastAlivetime (new date ()); // 소켓을 유지 ... SOCKETHOLDER.PUT (message.getfrom (), 래퍼); }}} return null; }} /** * chathandler * * @author yaolin */public class Chathandler는 Sockethandler {@override public 객체 핸들 (소켓 클라이언트, 객체 데이터) {if (data! = null) {chatmessage message = json.parseobject (data.tostring (), chatmessage.class); if (stringUtil.isnotempty (message.getfrom ()) && stringUtil.isnotempty (message.getto ())) {// evess & send if (socketholder.keyset (). contains (message.getfrom ())) {string 소유자 = message.getfrom (); Message.Setowner (소유자); // 소유자는 (constantValue.to_all.equals (message.getto ())) {// 일대일 // to_all 탭이 선택됩니다. message.setfrom (constantValue.to_all); for (string key : socketholder.keyset ()) {// 셀프 소켓 wrapper 래퍼 = socketholder.get (key)로 보냅니다. if (wrapper! = null) {sendHelper.Send (wrapper.getSocket (), message); }}} else {// 일대일 SocketWrapper wrapper = socketholder.get (message.getto ()); if (wrapper! = null) {// 소유자 = wendHelper.Send (wrapper.getSocket (), Message); // 또한 self로 보내기 // 탭으로 탭이 선택됩니다. message.setfrom (message.getto ()). setto (소유자); sendhelper.send (클라이언트, 메시지); }}}}} return null; }} 공개 클래스 파일 핸들러는 SOCKETHANDLER {@Override public Object Handle (소켓 클라이언트, 객체 데이터) {if (client! = null) {filemessage message = json.perseObject (data.toString (), filemessage.class); if (stringUtil.isnotEmpty (message.getFrom ()) && stringUtil.isnotEmpty (message.getto ()) {// evention & send if (socketholder.keyset (). contains (message.getfrom ())) {if (! constantValue.to_all.equals ()) {// One-One-One-One-One-One-One-all-all-all-all-all-all. socketholder.get (message.getto ()); if (wrapper! = null) {sendHelper.Send (wrapper.getSocket (), message); try {if (client! = null && wrapper.getSocket ()! = null && message.getsize ()> 0) {inputStream은 = client.getInputStream (); outputStream os = wrapper.getSocket (). getOutputStream (); int total = 0; while (! client.isclosed () &&! wrapper.getSocket (). isclosed ()) {if (is.available ()> 0) {byte [] buff = new Byte [constantValue.buff_size]; int len = -1; while (is.aveailable ()> 0 && (len = is.read (buff))! = -1) {os.write (buff, 0, len); 총 += 렌; loggerutil.debug ( "buff [" + len + "]"); } os.flush (); if (total> = message.getsize ()) {loggerutil.info ( "buff [ok]"); 부서지다; }}} // 파일을 보내 후 // 성공적으로 returnMessage result = new returnMessage (). setkey (key.tip) .setSuccess (true) .setContent (i18n.info_file_send_successly); result.setfrom (message.getto ()). setto (message.getfrom ()) .setowner (constantValue.server_name); SendHelper.Send (클라이언트, 결과); // 성공적으로 result.setContent (i18n.info_file_Receive_Successly) .setfrom (message.getfrom ()) .setto (message.getto ()); sendHelper.Send (wrapper.getSocket (), 결과); }} catch (예외 e) {loggerutil.error ( "핸들 파일 실패!" + e.getMessage (), e); }}}}}}}} return null; }} /** * loginHandler * * @author yaolin * */public class loginHandler 구현 SOCKETHANDLER {private usrService usrService = new UsrService (); @override public Object Handle (소켓 클라이언트, 객체 데이터) {returnMessage result = new returnMessage (); result.setSuccess (false); if (data! = null) {loginMessage message = json.parseobject (data.toString (), loginMessage.class); if (stringUtil.isNotEmpty (message.getUserName ()) && stringUtil.isnotempty (message.getPassword ())) {if (usrService.Login (useRservice.login (), message.getPassword ())! = null) {result.setsuccess (true); } else {result.setMessage (i18n.info_login_error_data); } result.setfrom (constantValue.server_name) .setto (message.getusername ()); } else {result.setMessage (i18n.info_login_empty_data); } // 로그인 result.setKey (key.login); if (result.issuccess ()) {// hold socket socketholder.put (result.getto (), new SocketWrapper (client, new date ()); } sendHelper.Send (클라이언트, 결과); if (result.issuccess ()) {// 목록을 송신 사용자 clientListUserDTO dto = new ClientListUserDto (); dto.setlistuser (socketholder.keyset ()); result.setContent (dto) .setKey (key.listuser); SendHelper.Send (클라이언트, 결과); }} return null; }} Public Class LogouthAndler는 Sockethandler {@override public 객체 핸들 (소켓 클라이언트, 객체 데이터) {if (data! = null) {logoutMessage message = json.parseobject (data.tostring (), logoutmessage.class); if (message! = null && stringutil.isnotempty (message.getfrom ())) {socketWrapper wrapper = socketholder.get (message.getfrom ()); Socket Socket = Wrapper.getSocket (); if (socket! = null) {try {socket.close (); 소켓 = null; } catch (예외 무시) {}} socketholder.remove (message.getfrom ()); }} return null; }} Public Class RegisterAndler는 Sockethandler {private usrservice usrservice = new Usrservice ()를 구현합니다. @override public Object Handle (소켓 클라이언트, 객체 데이터) {returnMessage result = new returnMessage (); result.setSuccess (false) .setfrom (constantValue.server_name); if (data! = null) {registermessage message = json.parseobject (data.tostring (), registermessage.class); if (stringUtil.isnotempty (message.getUserName ()) && stringUtil.isnotEmpty (message.getPassword ())) {if (usrService.register (message.getusername (), message.getpassword ())! = null) {result.setsuccess (true) .info_regeriter _. } else {result.setMessage (i18n.info_register_client_exist); }} else {result.setMessage (i18n.info_register_empty_data); } if (stringUtil.isnotempty (message.getUserName ())) {result.setto (message.getUername ()); } // register result.setkey (key.register) 이후; SendHelper.Send (클라이언트, 결과); } return null; }} /** * SendHelper를 사용하여 returnMessage를 보내기 (ReturnMessage) 데이터; if (stringUtil.isnotempty (message.getfrom ()) && stringUtil.isnotempty (message.getto ())) {socketWrapper wrap = socketholder.get (message.getto ()); if (wrap! = null) {sendHelper.Send (wrap.getSocket (), message); }}} return null; }}사용자 비즈니스 :
소켓 외에도 서버는 약간의 구체적인 비즈니스, 즉 사용자 등록, 로그인 등을 가지고 있습니다. 여기서는 두 가지 클래스의 USR과 USRService를 나열합니다. 이 비즈니스는 당분간 구현되지 않았습니다. 이 프로그램에 ORM 프레임 워크를 소개하려고하지 않으므로 Dbutil 세트를 작성하여 여기에 게시했습니다.
여기서 간단한 검증 만 수행되며 DB에 저장하는 것은 지속되지 않습니다. USR 및 USRService는 다음과 같습니다.
공개 클래스 usr {private long id; 개인 문자열 사용자 이름; 개인 문자열 비밀번호; public long getid () {return id; } public void setId (long id) {this.id = id; } public String getUserName () {return username; } public void setusername (String username) {this.username = username; } public String getPassword () {return password; } public void setpassword (문자열 비밀번호) {this.password = password; }} /** * // todo * @see yaolin.chat.server.usr.repository.usrrepository * @author yaolin */public class usrservice {// todo db private static map <string, usr> db = new Hashmap <String, usr> (); public usr register (문자열 사용자 이름, 문자열 암호) {if (stringUtil.isempty (username) || stringUtil.isempty (password)) {return null; } if (db.containskey (username)) {return null; // 존재하다; } usr usr = new usr (); usr.setusername (사용자 이름); usr.setpassword (md5util.getmd5code (password)); db.put (Username, USR); USR을 반환합니다. } public usr login (문자열 사용자 이름, 문자열 암호) {if (stringUtil.isempty (username) || stringUtil.isempty (password)) {return null; } if (db.containskey (username)) {usr usr = db.get (username); if (md5util.getmd5code (password) .equals (usr.getpassword ())) {return usr; }} return null; }} 다음은 dbutil 도구입니다.
/*** dbutils // todo를 조정하고 최적화해야합니다 !! * @author yaolin */public class dbutil {// 연결을 반복적으로 개인 정적 최종 목록 <connection> cache = new LinkedList <connection> (); 개인 정적 문자열 URL; 개인 정적 문자열 드라이버; 개인 정적 문자열 사용자; 개인 정적 문자열 비밀번호; 개인 정적 부울 디버그; static {inputStream은 = dbutil.class.getResourceasstream ( "/db.properties"); try {속성 p = new Properties (); p.load (IS); url = p.getProperty ( "url"); 드라이버 = p.getProperty ( "드라이버"); user = p.getProperty ( "사용자"); password = p.getProperty ( "비밀번호"); // 디버그를 위해 {debug = boolean.valueof (p.getProperty ( "Debug")); } catch (예외 무시) {debug = false; }} catch (예외 e) {throw new runtimeexception (e); } 마침내 {if (is! = null) {try {is.close (); is = null; } catch (예외 무시) {}}}} public synchronized static connection getConnection () {if (cache.isempty ()) {cache.add (makeConnection ()); } 연결 conn = null; int i = 0; try {do {conn = cache.remove (i); } while (conn! = null && conn.isclosed () && i <cache.size ()); } catch (예외 무시) {} try {if (conn == null || conn.isclosed ()) {cache.add (makeconnection ()); conn = cache.remove (0); } return conn; } catch (예외 e) {throw new runtimeexception (e); }} public synchronized static void close (Connection Connection) {try {if (connection! = null &&! connection.isclosed ()) {if (debug) debug ( "릴리스 연결!"); cache.add (연결); }} catch (sqlexception incore) {}} public static 객체 쿼리 (String SQL, resultStmapper Mapper, Object ... Args) {if (debug) debug (sql); 연결 conn = getConnection (); 준비된 상태 ps = null; resultSet rs = null; 객체 결과 = null; try {ps = conn.preparestatement (SQL); int i = 1; for (Object Object : args) {ps.setObject (i ++, object); } rs = ps.ExecuteQuery (); 결과 = mapper.mapper (rs); } catch (예외 e) {throw new runtimeexception (e); } 마침내 {try {if (rs! = null) {rs.close (); rs = null; } if (ps! = null) {ps.close (); ps = null; }} catch (예외 무시) {}} close (conn); 반환 결과; } public static int modify (String SQL, Object ... args) {if (debug) debug (sql); 연결 conn = getConnection (); 준비된 상태 ps = null; int row = 0; try {ps = conn.preparestatement (SQL); int i = 1; for (Object Object : args) {ps.setObject (i ++, object); } row = ps.ExecuteUpdate (); } catch (예외 e) {throw new runtimeexception (e); } 마침내 {try {if (ps! = null) {ps.close (); ps = null; }} catch (예외 무시) {}} close (conn); 반환 행; } public static int [] batch (list <string> sqls) {if (debug) debug (sqls.toString ()); 연결 conn = getConnection (); 문자 stmt = null; int [] 행; try {stmt = conn.createstatement (); for (string sql : sqls) {stmt.addbatch (sql); } row = stmt.executebatch (); } catch (예외 e) {throw new runtimeexception (e); } 마침내 {try {if (stmt! = null) {stmt.close (); stmt = null; }} catch (예외 무시) {}} close (conn); 반환 행; } public static int [] Batch (String SQL, ProadstatementSetter Setter) {if (Debug) Debug (SQL); 연결 conn = getConnection (); 준비된 상태 ps = null; int [] 행; try {ps = conn.preparestatement (SQL); setter.setter (ps); row = ps.Executebatch (); } catch (예외 e) {throw new runtimeexception (e); } 마침내 {try {if (ps! = null) {ps.close (); ps = null; }} catch (예외 무시) {}} close (conn); 반환 행; } private static connection makeConnection () {try {class.forname (driver) .newinstance (); Connection Conn = DriverManager.GetConnection (URL, USER, PASSFARPT); if (Debug) Debug ( "Create Connection!"); CONN을 반환; } catch (예외 e) {throw new runtimeexception (e); }} private static void debug (string sqls) {simpledateformat sdf = new simpledateformat ( "yyyy-mm-dd hh : mm : ss"); System.out.println (sdf.format (new date ()) + "debug" + thread.currentThread (). getId () + "--- [" + thread.currentThread (). getName () + "]" + "Excute SQLS :" + SQLS); }} /** * preparedStatementSetter * @Author yaolin */public interface propledStatementSetter {public void setter (preadstatement ps);} /** * resultSetMapper * @author yaolin */public interface resultSetMapper {public object mapper (resultset rs);} 소스 코드 다운로드 : 데모
위는이 기사의 모든 내용입니다. 모든 사람의 학습에 도움이되기를 바랍니다. 모든 사람이 wulin.com을 더 지원하기를 바랍니다.