티스토리 뷰

반응형

안녕하세요.

 

채팅앱 만들다가 갑자기 웬 서버냐구요? 

이제 로그인 창을 만들었으니 다른 유저와 채팅을 하기 위해서 간단한 서버를 만들어 볼겁니다.

 

우선 Windows Forms App (.NET)으로 생성합니다. (중요)UWP로 생성하면 안됩니다.

 

프로젝트 이름을 ServerChatting으로 지어줬습니다.

 

디자인은 이렇게 만들었습니다.

디버깅 문구는 Label 컨트롤러로 설정했으며

아래 TextBox는 멀티라인으로 설정해야 세로 크기가 늘어납니다. 그리고 Name을 txDebug로 설정했습니다.

 

이제 서버 코드를 작성해 봅시다.

아래 From1.cs 파일에서 코드 보기를 눌러줍니다.

 

 

반응형

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;

namespace ServerChatting
{
    public partial class Form1 : Form
    {
        // 클라이언트 정보 구조체
        struct User
        {
            public Socket sock;
            public string nick;
            public Thread threadReceive;
        }

        public Form1()
        {
            InitializeComponent();

            initView(); // 뷰 초기화
            initServer(); // 서버 초기화
        }


        string host = "127.0.0.1"; // 로컬 주소
        int nPort = 11561; // 포트 정보
        Socket listen_socket; // 서버 소켓 정보
        List<User> list_client = new List<User>(); // 클라이언트 정보를 리스트로 관리합니다.
        Thread listenThread; // 쓰레드를 추가하여 리슨 하도록 구현.

        private delegate void delegateSetLog(object msg);
        private void Logs(string _logs)
        {
            try
            {
                // Main Thread가 아닌 다른 Thread에서도 Control를 업데이트 할 수 있도록 함.
                delegateSetLog d = (msg) => { txDebug.Text += (string)msg + "\r\n"; };
                txDebug.Invoke(d, new object[] { _logs });
            }
            catch (Exception e)
            {
                txDebug.Text += _logs + "\r\n";
            }
        }

        private void initView()
        {
            // 뷰 초기화
            txDebug.Text = "";
        }

        private void initServer()
        {
            try
            {
                // 서버 초기화 구문.
                listen_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, nPort);

                listen_socket.Bind(endpoint);
                listen_socket.Listen(10);

                // 리슨은 block 모드 이므로 MainThread가 응답없음을 피하기 위해 쓰레드를 추가합니다.
                listenThread = new Thread(new ThreadStart(listenRun));
                listenThread.Start();

                Logs("SERVER START");
            }
            catch (Exception e)
            {
                Logs("ERROR:" + e.ToString());
            }
        }

        private void listenRun()
        {
            Socket tempSocket;
            while (true)
            {
                // block 모드로 클라이언트가 접속하기 전까지 멈춥니다.
                // 따라서 메인 쓰레드에서 실행하지 않고 별도로 쓰레드를 생성해서 클라이언트 접속을 기다립니다.
                tempSocket = listen_socket.Accept();

                // 클라이언트가 접속하면 user 구조체를 만들어 관리합니다.
                User user = new User();
                user.sock = tempSocket;
                
                Logs("CLIENT ACCEPT");
                // 클라이언트 리시버 쓰레드를 생성합니다.
                user.threadReceive = new Thread(new ParameterizedThreadStart(tcpReceive));
                user.threadReceive.Start(user.sock);

                list_client.Add(user);
            }
        }

        private void tcpReceive(object _socket)
        {
            // 쓰레드에서 무한루프 돌면서 데이터를 수신받습니다.

            Socket socket = _socket as Socket;
            string data;
            while (true)
            {
                byte[] bytes = new byte[1024];
                try
                {
                    // 데이터가 들어올때까지 block모드를 유지합니다.
                    int res = socket.Receive(bytes);

                    // 데이터를 받으면 string형태로 변환 시킵니다.
                    data = Encoding.UTF8.GetString(bytes, 0, res);

                    // 데이터가 0일 경우 클라이언트 종료 이벤트 입니다.
                    // 데이터가 0이하일 경우 소켓 오류 발생입니다.
                    if( res <= 0)
					{
                        // 클라이언트 종료
                        closeClient(socket);
                        break;
					}
                    if (data.IndexOf("/nick ") > -1) // 닉네임 정하는 명령어
                    {
                        data = data.Substring(6); // /nick 명령어 삭제, 넥네임만 data로 저장합니다.
                        // 유저 닉네임을 업데이트 합니다.
                        updateUserNick(socket, data);
                    }
                    else
                    {
                        // 전송한 유저가 누구인지 닉네임을 확인 후 메시지를 유저에게 전부 출력합니다.
                        findNickAndSend(socket, data);
                    }
                }
                catch(Exception e)
				{
                    // 클라이언트와 연결에 문제가 발생하여 종료 합니다.
                    for (int i = 0; i < list_client.Count; i++)
                    {
                        if (list_client[i].sock == socket)
                        {
                            socket.Close();
                            list_client.Remove(list_client[i]);
                        }
                    }
                    break;
				}
            }
        }

        private void updateUserNick(Socket _sock, string _newNick)
        {
            // 접속한 유저 중 닉네임을 업데이트 할 유저를 찾습니다.
            for (int i = 0; i < list_client.Count; i++)
            {
                if (list_client[i].sock == _sock)
                {
                    if (list_client[i].nick == null || list_client[i].nick.Equals(""))
                    {
                        // 초기 접속 시 닉네임이 없으므로 입장 메시지를 전송합니다.
                        sendBrodcast(_newNick + "님이 입장 하였습니다."); // 새로운 유저가 입장
                    }
                    else
                    {
                        // 닉네임 변경 메시지를 전달합니다.
                        sendBrodcast(list_client[i].nick + "님이 닉네임을 " + _newNick + "로 변경하였습니다."); // 닉네임 변경
                    }

                    // list_client[i].nick = _newNick // CS1612 ERROR
                    // https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/compiler-messages/cs1612

                    // 변경된 닉네임을 업데이트 합니다.
                    User mTemp = list_client[i];
                    mTemp.nick = _newNick;
                    list_client[i] = mTemp;
                }
            }
        }

        private void sendBrodcast(string _data)
        {
            // 접속한 클라이언트에게 메시지를 전달
            for (int i = 0; i < list_client.Count; i++)
            {
                list_client[i].sock.Send(Encoding.UTF8.GetBytes(_data));
            }
        }

        private void findNickAndSend(Socket _sock, string _data)
        {
            // 메시지를 보낸 유저의 닉네임을 찾아 전송합니다.
            for (int i = 0; i < list_client.Count; i++)
            {
                if (list_client[i].sock == _sock)
                {
                    sendBrodcast(list_client[i].nick + ":" + _data);
                }
            }
        }
        
        public void closeClient(Socket _sock)
		{
            // 유저 소켓을 닫습니다.
            for (int i = 0; i < list_client.Count; i++)
            {
                if (list_client[i].sock == _sock)
                {
                    _sock.Close();
                    list_client.Remove(list_client[i]);
                }
            }
        }
    }
}

 

코드가 약간 깁니다. 핵심은 Socket를 초기화 하고

접속한 유저를 User 구조체에로 관리합니다.

 

이 후 클라이언트 수정하여 접속을 시도해 봅시다.

 

 

UWP를 사용하지 않은 이유 (MSDN 참고)

대충 이런 내용입니다. UWP앱에서 로컬 루프백을 지원하지 않은 것 같으니 콘솔앱이나, 닷넷 윈도우 폼 으로 제작하시면 됩니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함