C# Winform을 사용한 팀 프로젝트입니다. (2023. 02. 08 수 ~ 2023. 02. 15 수)
프로젝트 이름 : 나는 피카소 너는 마추소
1. 데이터베이스 테이블에서 제시어를 받아오기 위한 DB 생성 및 서버와 연동 (Oracle / DB생성 :Sql Developer사용 )
2. 소켓(Socket) 프로그래밍을 이용한 네트워크 프로그래밍
3. 멀티스레드 프로그래밍을 통한 다중 클라이언트 및 다중 인원 그리기 기능
4. 소켓 서버와 클라이언트 연동 (DB 서버 연동 포함)
5. 클라이언트 서버 구조를 유지하며 연동하여 구현했다.
개요, 요구사항, 기능
개요 및 요구사항
원래는 클라이언트_1에서 그리는 사람들이 있고, 클라이언트_2에서 맞히는 사람 한명으로 정하여 정답을 입력해서 맞히는 방식으로 설계하려했다.
하지만 프로젝트 발표에서 보는 사람들 모두가 참여하도록 하기위해 정답 입력 대신 그림을 보는 모든 사람들이 그림과 힌트가 제공되는 클라이언트_2에서 맞히는 것으로 설계하기로 했다.
클라이언트_1의 여러 사용자가 한 서버에 접속하여 동시에 제시어에 대한 그림을 그린다.
클라이언트_2는 문제를 맞히는 입장으로
클라이언트_1과 비슷하지만 그림을 그릴 수 없고, 클라이언트_1에서 그려진 내용들을 보는 것만 가능하다.
클라이언트_1과 다르게 처음부터 제시어를 볼 수 없고, 그려진 내용을 보고 정답을 유추해야한다.
일정 시간이 지나면 힌트와 글자수가 나온다.
리셋을 누르면 Panel의 그림판을 초기화하고 게임을 새로 시작할 수 있다.
기능
클라이언트_1 : 그림을 그리는 사람들 클라이언트
클라이언트_2 : 그림을 보고 맞히는 사람들 클라이언트
핵심 기능 : 여러 사용자가 동시에 그리기 위한 다중 드로잉/다중 그림판, DB서버의 제시어 테이블을 받아와서 출력
부가 기능 : 타이머를 이용해 경과 시간 및 시간이 흐르면 힌트, 글자 수 출력
그 외 기능 : 팔레트를 통한 색 변경, 지우개, 화면 전체 지우기 (새 게임)
형상 관리 및 일정 계획
소스트리를 사용하여 깃허브에서 관리하고, 회의 및 계획은 노션으로 작성하며 진행하였다.
맡은 역할
다중 그리기 및 서버 클라이언트_1 병합 및 연동
느낀점
처음엔 서버와 클라이언트 연동하여 그림을 그리는 것도 어려움이 있었다. 하지만 서버와 클라이언트의 구조에 대해 점차 알게되면서 구현되었고, 다음 문제점인 다중 드로잉 또한 C#의 Dictionary를 이용하여 ID에 따라 좌표를 주어서 여러명이 동시에 그릴 수 있도록 구현하였다. 클라이언트_2를 맡은 팀원과 같이 멀티스레드를 활용하여 구현해야 한다는 것을 주로 피드백했고, 필수 기능을 구현할 수 있었다. 구현하면서 생긴 오류들은 주로 Console.WriteLine을 사용하여 어느 부분이 문제인지 출력하여 디버깅하였고 대부분 출력을 통한 디버깅으로 오류(아이디 정보에 대한 null오류, 색변환, 제시어 전송 등)를 해결할 수 있었다.
UI
서버 (Server)
DBConnect, DisConnect - DB서버와 연결하고 연결을 해제한다.
전체 출력 - DB서버의 저장된 테이블들을 전부 출력한다. (확인용)
제시어 출력 - DB서버의 저장된 제시어 테이블에서 하나를 랜덤으로 출력하고 나머지 클라이언트에도 보낸다.
클라이언트 1 (Client 1) - 그리는 사람들
상단에는 타이머를 나타낼 빈 공간 (Form_Paint), 제시어와 제시어 힌트를 보여줄 세개의 텍스트 박스가 있다.
Link Start! : 서버에 연결한다.
RE : Panel의 그림판에 그려진 내용을 전부 지우고 제시어와 타이머를 초기화한다.
하단에는 그림을 그리는 사람들끼리 채팅을 하는 UI이며 ID를 입력하고 채팅을 칠 수 있다.
클라이언트 2 (Client 2) - 보고 맞추는 사람들
클라이언트1과 비슷하지만 그림을 그릴 수 없고, 클라이언트1에서 그려진 내용들을 보는 것만 가능하다.
클라이언트1과 다르게 처음부터 제시어를 볼 수 없고, 그려진 내용을 보고 정답을 유추해야한다.
일정 시간이 지나면 힌트와 글자수가 나온다.
리셋을 누르면 Panel의 그림판을 초기화하고 게임을 새로 시작할 수 있다.
코드 분석
서버
내용 : 서버 전체 코드, 서버 멀티 스레드, 제시어 출력, 제시어 데이터보내기
서버 전체 코드
코드 보기
using Oracle.ManagedDataAccess.Client;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
namespace Project_Picasso_Server
{
public partial class Form1 : Form
{
// DB연동
OracleConnection conn = null;
private string dbConnInfo = @"Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521)))
(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=xe))); User Id = project; Password = 123456;";
Random rand = new Random();
// 서버구현
Socket serverSocket; // 클라이언트의 접속 연결을 담당하는
IPEndPoint ipep; // 서버의 주소(ip, port)
Thread threadAccept; // 연결 담당 스레드
bool isRunAccept = true;
IList<Socket> clientList = new List<Socket>();
object keyObj = new object();
delegate void AddMsgData(string log);
AddMsgData addMsgData = null;
string word_answer = "";
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
this.FormClosed += Form1_FormClosed;
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
button_DBConnect.PerformClick();
serverSocket.Close();
}
private void Form1_Load(object sender, EventArgs e)
{
button_DisConnect.Enabled = false;
this.addMsgData = AddClinetLogListBox;
}
void BroadCastData(Socket excludingSocket, string data)
{
foreach (var connSocket in this.clientList)
{
// 이 소켓은 제외하고 나머지에만 데이터를 보낸다.
if (connSocket == excludingSocket)
continue;
NetworkStream ns = new NetworkStream(connSocket);
StreamWriter sw = new StreamWriter(ns);
sw.WriteLine(data);
sw.Flush();
}
}
void BroadCastData_Word(string word)
{
foreach (var connSocket in this.clientList)
{
// 이 소켓은 제외하고 나머지에만 데이터를 보낸다.
NetworkStream ns = new NetworkStream(connSocket);
StreamWriter sw = new StreamWriter(ns);
sw.WriteLine(word);
sw.Flush();
}
}
private void AddDBLogListBox(string result)
{
if (listBox_DBlog.InvokeRequired)
{
Invoke(addMsgData, new object[] { result });
}
else
{
listBox_DBlog.Items.Add(result);
listBox_DBlog.SelectedIndex = listBox_DBlog.Items.Count - 1;
}
}
void AddClinetLogListBox(string log)
{
if (listBox_Clientlog.InvokeRequired)
{
Invoke(addMsgData, new object[] { log });}
else
{
listBox_Clientlog.Items.Add(log);
listBox_Clientlog.SelectedIndex = listBox_Clientlog.Items.Count - 1;
}
}
private void button_DBConnect_Click(object sender, EventArgs e)
{
string strconn = this.dbConnInfo;
conn = new OracleConnection(strconn);
try
{
conn.Open();
if (conn.State == ConnectionState.Open)
{
AddDBLogListBox("Oracle Server 연결 성공");
}
}
catch (Exception ex)
{
conn = null;
AddDBLogListBox("DB Error : " + ex.Message);
}
this.serverSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
this.ipep = new IPEndPoint(IPAddress.Any, 9000);
this.serverSocket.Bind(this.ipep);
this.serverSocket.Listen(100);
AddClinetLogListBox("서버 시작");
this.isRunAccept = true;
this.threadAccept = new Thread(new ThreadStart(ThreadAccept));
this.threadAccept.Start();
button_DBConnect.Enabled = false;
button_DisConnect.Enabled = true;
}
private void button_DisConnect_Click(object sender, EventArgs e)
{
if (conn != null &&
conn.State == ConnectionState.Open ||
conn.State == ConnectionState.Connecting)
{
conn.Close();
conn = null;
AddDBLogListBox("Oracle 연결 해제");
}
this.serverSocket.Close();
button_DBConnect.Enabled = true;
button_DisConnect.Enabled = false;
foreach (var connSocket in clientList)
{
connSocket.Close();
}
}
private void button1_Click(object sender, EventArgs e)
{
string selectSql = "SELECT * FROM CAT";
try
{
OracleCommand cmd = new OracleCommand();
// 연결객체와 sql문 전달
cmd.Connection = this.conn;
cmd.CommandText = selectSql;
// sql문 실행후 결과를 reader객체에 저장
OracleDataReader reader = cmd.ExecuteReader();
//emp테이블의 컬럼 정보 얻기
string[] columns = new string[reader.FieldCount];
for (int i = 0; i < reader.FieldCount; i++)
{
columns[i] = reader.GetName(i);
AddDBLogListBox("컬럼명_" + i + ":" + columns[i]);
}
AddDBLogListBox("");
// 행(레코드)데이터를 가져옴
// 더 이상 읽을 레코드가 없으면 false를 반환
while (reader.Read())
{
string[] datas = new string[reader.FieldCount];
for (int i = 0; i < reader.FieldCount; i++)
{
datas[i] = reader.GetValue(i).ToString();
string result = String.Format($"{columns[i]} : {datas[i]}");
AddDBLogListBox(result);
}
AddDBLogListBox("");
}
reader.Close();
cmd.Dispose(); // 리소스 해제
}
catch (Exception ex)
{
AddDBLogListBox("DB Error : " + ex.Message);
}
}
private void button2_Click(object sender, EventArgs e)
{
int rnum = rand.Next(15);
string selectSql = $"SELECT * FROM CAT WHERE pno = {rnum}";
try
{
OracleCommand cmd = new OracleCommand();
cmd.Connection = this.conn;
cmd.CommandText = selectSql;
OracleDataReader reader = cmd.ExecuteReader();
string[] columns = new string[reader.FieldCount];
for (int j = 0; j < reader.FieldCount; j++)
{
columns[j] = reader.GetName(j);
}
while (reader.Read())
{
string[] datas = new string[reader.FieldCount];
for (int i = 0; i < reader.FieldCount; i++)
{
datas[i] = reader.GetValue(i).ToString();
string result = String.Format($"{columns[i]} : {datas[i]}");
AddDBLogListBox(result);
}
AddDBLogListBox("");
//마지막 0 번호 , 1 힌트, 2 글자수 , 3 정답
datas[3] = reader.GetValue(3).ToString();
word_answer = String.Format($"{datas[1]}|{datas[2]}|{datas[3]}");
AddDBLogListBox(word_answer);
BroadCastData_Word(word_answer);
}
reader.Close();
cmd.Dispose();
}
catch (Exception ex)
{
AddDBLogListBox("DB Error : " + ex.Message);
}
}
void ThreadRecv(object socket)
{
Socket connSocket = (Socket)socket;
NetworkStream ns = new NetworkStream(connSocket);
StreamReader sr = new StreamReader(ns);
StreamWriter sw = new StreamWriter(ns);
while (true)
{
// 데이터 수신
try
{
AddClinetLogListBox("클라이언트 데이터 수신 대기...");
string data = sr.ReadLine();
AddClinetLogListBox($"수신 : {data}");
//string rdata =
if (data == null)
{
break;
}
// 받은 데이터를 연결된 나머지 클라이언트에 보낸다.
lock (this.keyObj)
{
BroadCastData(connSocket, data);
}
}
catch (Exception ex)
{
AddClinetLogListBox($"Recv Error : {ex.Message}");
break;
}
}
AddClinetLogListBox($"{connSocket} 클라이언트 접속 종료");
lock (this.keyObj)
{
clientList.Remove(connSocket);
}
}
void ThreadAccept()
{
while (this.isRunAccept)
{
try
{
AddClinetLogListBox("클라이언트 접속 대기...");
Socket connSocket = this.serverSocket.Accept();
AddClinetLogListBox("클라이언트 접속 연결!!!");
// 새로 연결된 소켓을 클라이언트 소켓 리스트에 등록한다.
lock (this.keyObj)
{
clientList.Add(connSocket);
}
// 새로운 클라이언트가 접속하면 담당 스레드를 만들어서
// 입출력 처리하도록 한다
Thread threadRecv = new Thread(new ParameterizedThreadStart(ThreadRecv));
threadRecv.Start(connSocket);
}
catch (Exception ex)
{
this.isRunAccept = false;
AddClinetLogListBox($"Accept Error : {ex.Message}");
}
}
}
}
}
서버 멀티 스레드
코드 보기
void ThreadRecv(object socket)
{
Socket connSocket = (Socket)socket;
NetworkStream ns = new NetworkStream(connSocket);
StreamReader sr = new StreamReader(ns);
StreamWriter sw = new StreamWriter(ns);
while (true)
{
// 데이터 수신
try
{
AddClinetLogListBox("클라이언트 데이터 수신 대기...");
string data = sr.ReadLine();
AddClinetLogListBox($"수신 : {data}");
//string rdata =
if (data == null)
{
break;
}
// 받은 데이터를 연결된 나머지 클라이언트에 보낸다.
lock (this.keyObj)
{
BroadCastData(connSocket, data);
}
}
catch (Exception ex)
{
AddClinetLogListBox($"Recv Error : {ex.Message}");
break;
}
}
AddClinetLogListBox($"{connSocket} 클라이언트 접속 종료");
lock (this.keyObj)
{
clientList.Remove(connSocket);
}
}
void ThreadAccept()
{
while (this.isRunAccept)
{
try
{
AddClinetLogListBox("클라이언트 접속 대기...");
Socket connSocket = this.serverSocket.Accept();
AddClinetLogListBox("클라이언트 접속 연결!!!");
// 새로 연결된 소켓을 클라이언트 소켓 리스트에 등록한다.
lock (this.keyObj)
{
clientList.Add(connSocket);
}
// 새로운 클라이언트가 접속하면 담당 스레드를 만들어서
// 입출력 처리하도록 한다
Thread threadRecv = new Thread(new ParameterizedThreadStart(ThreadRecv));
threadRecv.Start(connSocket);
}
catch (Exception ex)
{
this.isRunAccept = false;
AddClinetLogListBox($"Accept Error : {ex.Message}");
}
}
}
제시어 출력
제시어를 랜덤하게 출력하기 위해 Random함수를 사용하여 WHERE pno = {rnum}으로 작성하였다.
pno (제시어들의 번호)를 rnum으로 랜덤하게 뽑을 수 있도록 하였다.
코드 보기
private void button2_Click(object sender, EventArgs e) //제시어 출력 버튼
{
int rnum = rand.Next(15);
string selectSql = $"SELECT * FROM CAT WHERE pno = {rnum}";
try
{
OracleCommand cmd = new OracleCommand();
cmd.Connection = this.conn;
cmd.CommandText = selectSql;
OracleDataReader reader = cmd.ExecuteReader();
string[] columns = new string[reader.FieldCount];
for (int j = 0; j < reader.FieldCount; j++)
{
columns[j] = reader.GetName(j);
}
while (reader.Read())
{
string[] datas = new string[reader.FieldCount];
for (int i = 0; i < reader.FieldCount; i++)
{
datas[i] = reader.GetValue(i).ToString();
string result = String.Format($"{columns[i]} : {datas[i]}");
AddDBLogListBox(result);
}
AddDBLogListBox("");
//마지막 0 번호 , 1 힌트, 2 글자수 , 3 정답
datas[3] = reader.GetValue(3).ToString();
word_answer = String.Format($"{datas[1]}|{datas[2]}|{datas[3]}");
AddDBLogListBox(word_answer);
BroadCastData_Word(word_answer);
}
reader.Close();
cmd.Dispose();
}
catch (Exception ex)
{
AddDBLogListBox("DB Error : " + ex.Message);
}
}
datas[0] : 제시어의 번호
datas[1] : 제시어의 힌트
datas[2] : 제시어의 글자 수
datas[3] : 제시어
word_answer = String.Format($"{datas[1]}|{datas[2]}|{datas[3]}");으로 datas들을 나눠주었다.
(따로 String.Format을 '|'으로 구분하였는데 하지않으면 클라이언트에서 Json으로 변환될 때 ',' 반점이 많아 데이터가 정확히 구분되지 않고 저장이 되었다)
AddDBLogListBox(word_answer); : 서버 UI에서 제시어를 확인합니다.
BroadCastData_Word(word_answer); : 클라이언트로 제시어를 보내는 역할입니다.
코드 보기
//마지막 0 번호 , 1 힌트, 2 글자수 , 3 정답
datas[3] = reader.GetValue(3).ToString();
word_answer = String.Format($"{datas[1]}|{datas[2]}|{datas[3]}");
AddDBLogListBox(word_answer);
BroadCastData_Word(word_answer);
제시어 데이터 보내기
매개변수 word로 입력받은 제시어 데이터를 보내는 역할을 합니다.
코드 보기
void BroadCastData_Word(string word)
{
foreach (var connSocket in this.clientList)
{
NetworkStream ns = new NetworkStream(connSocket);
StreamWriter sw = new StreamWriter(ns);
sw.WriteLine(word);
sw.Flush();
}
}
클라이언트_1
내용 : 클라이언트_1 전체 코드, 클라이언트 패킷, 패널(Panel), ID에 따라 색 변경
클라이언트_1 전체 코드
코드 보기
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Text.Json;
using System.Threading;
using Project_Picasso_Client_1.Properties;
using System.Runtime.Remoting.Messaging;
using System.Drawing.Drawing2D;
using System.Text.RegularExpressions;
using System.Media;
namespace Project_Picasso_Client_1
{
public partial class Form1 : Form
{
class CmdPacket
{
public string ID { get; set; }
public char CMD { get; set; }
}
class ChatPacket : CmdPacket
{
public string CHATDATA { get; set; }
}
class PositionPacket : CmdPacket
{
// CMD = 'P';
public int X { get; set; }
public int Y { get; set; }
}
class ClickPacket : CmdPacket
{
// CMD = 'C';
public bool CLICK { get; set; }
public int X { get; set; }
public int Y { get; set; }
}
class ColorPacket : CmdPacket
{
public int COLOR { get; set; }
}
//서버 관련 필드
Socket clientSocket;
IPEndPoint ipep; // 서버의 접속 주소
bool isRunRecv = true; // 수신 스레드의 계속 동작 여부
NetworkStream ns;
StreamWriter sw;
StreamReader sr;
delegate void AddMsgLogData(string data);
AddMsgLogData addMsgLogData = null;
delegate void StopState();
StopState stopButtonState = null;
Dictionary<string, Point> playerLocation = new Dictionary<string, Point>();
Dictionary<string, Point> playerLocation_End = new Dictionary<string, Point>();
//색 변환
Dictionary<string, int> player_int = new Dictionary<string, int>();
Dictionary<string, Color> player_Color = new Dictionary<string, Color>();
//정답
string word = null;
string hint = null;
string Long = null;
//그래픽 패널 필드
//Graphics g;
int dr = 2;
//Pen pen;
Pen client_pen;
Pen server_pen;
Color server_color = Color.Black;
//노래
private SoundPlayer Player = new SoundPlayer();
//타이머 필드
int min = 0;
int sec = 0;
System.Windows.Forms.Timer tm = new System.Windows.Forms.Timer();
System.Windows.Forms.Timer tm1 = new System.Windows.Forms.Timer();
public Form1()
{
InitializeComponent();
//드로잉
client_pen = new Pen(Color.Black, dr);
server_pen = new Pen(Color.Black, dr);
//서버관련
this.DoubleBuffered = true;
this.Load += Form1_Load;
this.FormClosed += Form1_FormClosed;
//타이머
tm.Interval = 1000;
tm1.Interval = 1000;
tm.Tick += Tm_Tick;
tm1.Tick += Tm1_Tick;
this.Paint += Form1_Paint;
}
//타이머 그리기
private void Form1_Paint(object sender, PaintEventArgs e)
{
Font ft = new Font("맑은고딕", 15, FontStyle.Bold);
e.Graphics.DrawString($"{min}분{sec}초 지났습니다",
ft, Brushes.Black, 270, 20);
}
// 타이머
private void Tm1_Tick(object sender, EventArgs e)
{
if (textBox1.Text == word)
{
tm.Start();
}
}
private void Tm_Tick(object sender, EventArgs e)
{
sec++;
if (sec == 60)
{
min++;
if (min == 2)
{
tm.Stop();
//tm1.Stop();
}
sec = 0;
}
Invalidate();
}
private void time_reset()
{
sec = 0;
min = 0;
min.ToString("00");
sec.ToString("00");
Invalidate();
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
this.isRunRecv = false; // 추가
button_exit.PerformClick();
}
private void Form1_Load(object sender, EventArgs e)
{
this.addMsgLogData = AddMsgLogListBox;
this.stopButtonState = StopButtonState;
try
{
//노래재생
this.Player.SoundLocation = @"../../Resources/maple_BGM.wav";
this.Player.PlayLooping();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error playing sound");
}
tm1.Start();
textBox1.Enabled = true;
textBox2.Enabled = true;
textBox3.Enabled = true;
button_exit.Enabled = false;
textBox_Chat.Enabled = false;
}
void AddMsgLogListBox(string data)
{
if (listBox_ChatView.InvokeRequired)
{
Invoke(addMsgLogData, new object[] { data });
}
else
{
listBox_ChatView.Items.Add(data);
listBox_ChatView.SelectedIndex = listBox_ChatView.Items.Count - 1;
}
}
void ThreadRecv()
{
while (this.isRunRecv)
{
//string data = null;
try
{
string data = sr.ReadLine();
// DB 제시어만 따로 받아주는 if문
if (data.Contains('|'))
{
string[] rdata = data.Split('|');
hint = rdata[0];
Long = rdata[1];
word = rdata[2];
textBox1.Text = word;
textBox2.Text = hint;
textBox3.Text = Long;
}
if (data == null)
{
this.isRunRecv = false;
}
else
{
// json문자열을 원래의 객체로 복원
// 일단 모든 패킷 클래스의 부모 클래스로 변환하여
// 명령의 종류를 확인한 후
// 어떤 자식 클래스로 변환할 지 결정함
CmdPacket cmd = JsonSerializer.Deserialize<CmdPacket>(data);
switch (cmd.CMD)
{
// 명령을 확인한 후 어떤 클래스로 변환할 지 결정함
case 'P':
PositionPacket pp = JsonSerializer.Deserialize<PositionPacket>(data);
ColorPacket colp = JsonSerializer.Deserialize<ColorPacket>(data);
Graphics g = panel_Drawing.CreateGraphics();
g.SmoothingMode = SmoothingMode.AntiAlias;
if (playerLocation.ContainsKey(textBox_ID.Text))
{
//server_color = ColorTranslator.FromOle(colp.COLOR);
if (player_Color.ContainsKey(cmd.ID) == false)
{
g.DrawLine(Pens.Black, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
continue;
}
else if (player_Color[cmd.ID] == Color.White)
{
Pen erase_pen = new Pen(Color.White, 20);
g.DrawLine(erase_pen, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
continue;
}
//Console.WriteLine(player_Color[cmd.ID]);
server_pen = new Pen(player_Color[cmd.ID]);
// Console.WriteLine($"P if {player_Color[cmd.ID]}");
g.DrawLine(server_pen, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
}
break;
case 'C':
ClickPacket cp = JsonSerializer.Deserialize<ClickPacket>(data);
if (playerLocation.ContainsKey(textBox_ID.Text))
{
playerLocation_End[cmd.ID] = new Point(cp.X, cp.Y);
}
//else
//{
// MessageBox.Show("색또는 아이디를 지정해주세요");
//}
break;
case 'R':
colp = JsonSerializer.Deserialize<ColorPacket>(data);
if (player_Color.ContainsKey(cmd.ID))
{
player_Color[cmd.ID] = ColorTranslator.FromOle(colp.COLOR);
Console.WriteLine(player_Color[cmd.ID]);
if (player_Color[cmd.ID] == Color.Empty)
{
Console.WriteLine($"R if 색이 빈 값입니다.");
server_pen = new Pen(Color.Black); //검은색 기본값
}
else
{
Console.WriteLine($"R else {player_Color[cmd.ID]}");
server_pen = new Pen(player_Color[cmd.ID]);
//pen = new Pen(colp.COLOR, dr);
}
}
else
{
Color Trans_Color = ColorTranslator.FromOle(colp.COLOR);
player_Color.Add(cmd.ID, Trans_Color);
}
break;
case 'T':
ChatPacket chatp = JsonSerializer.Deserialize<ChatPacket>(data);
//AddMsgLogListBox($"수신 >> {chatp.CHATDATA}");
AddMsgLogListBox($"{chatp.ID} >> {chatp.CHATDATA}");
break;
}
}
}
catch (JsonException ex)
{
AddMsgLogListBox($"Json Error : {ex.Message}");
//AddMsgLogListBox($"Json Data : {data}");
}
catch (Exception ex)
{
AddMsgLogListBox($"Recv Error : {ex.Message}");
// this.isRunRecv = false;
}
}
Invoke(stopButtonState, null);
}
void StopButtonState()
{
button_Link.Enabled = true;
button_exit.Enabled = false;
textBox_Chat.Enabled = false;
}
private void button_Link_Click(object sender, EventArgs e)
{
this.clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
this.ipep = new IPEndPoint(IPAddress.Parse("192.168.0.26"), 9000);
AddMsgLogListBox("서버 접속 시도...");
this.clientSocket.Connect(ipep);
AddMsgLogListBox("서버 접속 연결!!!");
this.ns = new NetworkStream(this.clientSocket);
this.sw = new StreamWriter(this.ns);
this.sr = new StreamReader(this.ns);
this.ActiveControl = textBox_ID;
this.isRunRecv = true;
Thread threadRecv = new Thread(new ThreadStart(ThreadRecv));
threadRecv.Start();
button_Link.Enabled = false;
button_exit.Enabled = true;
textBox_Chat.Enabled = true;
}
private void button_RE_Click(object sender, EventArgs e)
{
Graphics g = panel_Drawing.CreateGraphics();
textBox1.Text = null;
textBox2.Text = null;
textBox3.Text = null;
tm.Dispose();
// 2
panel_Drawing.Invalidate();
time_reset();
}
private void button_exit_Click(object sender, EventArgs e)
{
if (this.clientSocket != null &&
this.clientSocket.Connected)
{
this.clientSocket.Close();
}
button_Link.Enabled = true;
button_exit.Enabled = false;
textBox_Chat.Enabled = false;
}
// 패킷 전송
void SendPositionPacket(string id, int x, int y)
{
PositionPacket pp = new PositionPacket();
pp.CMD = 'P';
pp.ID = id;
pp.X = x;
pp.Y = y;
string data = JsonSerializer.Serialize(pp);
sw.WriteLine(data);
sw.Flush();
}
void SendClickPacket(bool isClick, string id, int x, int y)
{
// 서버로 ClickPacket 전송
ClickPacket cp = new ClickPacket();
cp.CMD = 'C'; // CMD
cp.CLICK = isClick;
cp.ID = id;
cp.X = x;
cp.Y = y;
string data = JsonSerializer.Serialize(cp);
sw.WriteLine(data);
sw.Flush();
}
// 패널 그리기 [자신]
private void panel_Drawing_MouseMove(object sender, MouseEventArgs e)
{
Graphics g = this.panel_Drawing.CreateGraphics();
if (e.Button == MouseButtons.Left) // 왼클릭 상태일 때
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/move {id}, {e.Location}"); // 디버깅 출력용
//client_pen
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawLine(client_pen, playerLocation[id], e.Location);
SendPositionPacket(id, playerLocation[id].X, playerLocation[id].Y);
playerLocation[id] = e.Location;
}
}
}
private void panel_Drawing_MouseDown(object sender, MouseEventArgs e)
{
Console.WriteLine("MouseDown");
if (e.Button == MouseButtons.Left)
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/Down {id}, {e.Location}"); // 디버깅 출력용
playerLocation[id] = e.Location;
SendClickPacket(true, id, playerLocation[id].X, playerLocation[id].Y);
}
else
{
//Console.WriteLine($"else/Down {id}, {e.Location}"); //디버깅 출력용
playerLocation.Add(id, e.Location);
//player_Color.Add(id, Color.Black); // 기본색 추가
SendClickPacket(true, id, playerLocation[id].X, playerLocation[id].Y);
}
}
}
private void panel_Drawing_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/UP {id}, {e.Location}"); //디버깅 출력용
playerLocation[id] = e.Location;
SendPositionPacket(id, playerLocation[id].X, playerLocation[id].Y);
SendClickPacket(false, id, playerLocation[id].X, playerLocation[id].Y); // id
}
}
}
// 펜 색변경
private void toolStripButton_Color_Click(object sender, EventArgs e)
{
ClickPacket cp = new ClickPacket();
ColorDialog cd = new ColorDialog();
if (cd.ShowDialog() == DialogResult.OK)
{
Color color = cd.Color;
client_pen = new Pen(color, dr);
int oleColor = ColorTranslator.ToOle(color);
ColorPacket colp = new ColorPacket();
colp.CMD = 'R';
string id;
id = textBox_ID.Text; // id
colp.ID = id;
if (player_int.ContainsKey(id))
{
colp.COLOR = oleColor;
player_int[id] = oleColor;
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
else
{
colp.COLOR = oleColor;
player_int.Add(id, oleColor);
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
}
}
private void toolStripButton_E_Click(object sender, EventArgs e)
{
Graphics g = panel_Drawing.CreateGraphics();
Color color = Color.White;
client_pen = new Pen(color, 20);
int oleColor = ColorTranslator.ToOle(color);
ColorPacket colp = new ColorPacket();
colp.CMD = 'R';
string id;
id = textBox_ID.Text; //
colp.ID = id;
if (player_int.ContainsKey(id))
{
colp.COLOR = oleColor;
player_int[id] = oleColor;
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
else
{
colp.COLOR = oleColor;
player_int.Add(id, oleColor);
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
}
// 채팅
private void textBox_Chat_KeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Enter:
ChatPacket chatp = new ChatPacket();
chatp.CMD = 'T';
chatp.ID = textBox_ID.Text;
chatp.CHATDATA = textBox_Chat.Text;
// json문자열로 변환
string data = JsonSerializer.Serialize(chatp);
this.sw.WriteLine(data);
this.sw.Flush();
AddMsgLogListBox($"{chatp.ID} : {chatp.CHATDATA}");
// AddMsgLogListBox($"전송 : {data}");
textBox_Chat.Clear();
break;
}
}
}
}
클라이언트 패킷
클라이언트에서 전송할 데이터 정보를 패킷형태로 구현하였다.
CmdPacket : 부모클래스이며 ID를 가지고 있으며 CMD에 따라 패킷의 종류가 나뉜다.
ChatPacket : 채팅데이터
PositionPacket : 마우스 위치 데이터
ClickPacket : 마우스 클릭 여부 및 위치 데이터 (클릭 여부 true, false는 사용되지 않고 있으나 기능이 추가될 때 사용될 것을 고려하여 추가해두었다)
ColorPacket ['R'] : Pen색 변경 데이터
(Color형이 아닌 int형으로 선언되어있는데, 이부분은 Json변환에 있어 Color형은 변환이 되지않는 문제가 있었다.
그래서 Color를 ToOle로 변환하여 int로 저장한 후 패킷으로 전송하여 연동된 클라이언트들에 전송할 때 다시 Json으로 변환하고
FromOle로 다시 Color를 바꿔주는 과정을 진행하였다.)
SendPositionPacket ['P'] : 마우스의 위치 정보를 ID와 함께 Json형태로 변환하여 전송한다
SendClickPacket ['C']: 마우스의 클릭여부와 ID, 위치 정보를 Json형태로 변환하여 전송한다.
코드 보기
class CmdPacket
{
public string ID { get; set; }
public char CMD { get; set; }
}
class ChatPacket : CmdPacket
{
public string CHATDATA { get; set; }
}
class PositionPacket : CmdPacket
{
// CMD = 'P';
public int X { get; set; }
public int Y { get; set; }
}
class ClickPacket : CmdPacket
{
// CMD = 'C';
public bool CLICK { get; set; }
public int X { get; set; }
public int Y { get; set; }
}
class ColorPacket : CmdPacket
{
public int COLOR { get; set; }
}
// 패킷 전송
void SendPositionPacket(string id, int x, int y)
{
PositionPacket pp = new PositionPacket();
pp.CMD = 'P';
pp.ID = id;
pp.X = x;
pp.Y = y;
string data = JsonSerializer.Serialize(pp);
sw.WriteLine(data);
sw.Flush();
}
void SendClickPacket(bool isClick, string id, int x, int y)
{
ClickPacket cp = new ClickPacket();
cp.CMD = 'C'; // CMD
cp.CLICK = isClick;
cp.ID = id;
cp.X = x;
cp.Y = y;
string data = JsonSerializer.Serialize(cp);
sw.WriteLine(data);
sw.Flush();
}
패널(Panel)에 그림을 그리는 부분 (자신한테만 보이며 해당 데이터를 패킷으로 전송)
id : 클라이언트들의 ID
e.Location : 패널의 마우스 현재 좌표
패널이 MouseDown상태이며 왼쪽 클릭(MouseButtons.Left)을 했다면, playerLocation.ContainsKey(id)의 여부에 따라 if문을 조건으로 주었다.
만약 id라는 Key를 가지고 있다면, 그대로 playerLocation[id] = e.Location;되어 id라는 Key값에 e.Location이 저장되고 id라는 Key값이 없을 경우 playerLocation.Add(id, e.Location);에서 Add로 id와 e.Location를 추가한다.
(여기서 조건을 주지않으면, 처음에는
Dictionary<string, Point> playerLocation = new Dictionary<string, Point>();
키값이 null이기 때문에 오류가 발생한다. 그러므로 null일 때는 Add로 따로 추가해주어야 한다)
'SendClickPacket'을 통해 서버로 보낼 아이디와 좌표에 대한 정보를 보냅니다.
이후 MouseMove와 MouseUp은 비슷하기 때문에 'SendClickPacket'와 'SendPositionPacket'만 다르다.
MouseMove : 마우스가 움직일 때 마다 'SendPositionPacket' 마우스의 위치 정보를 패킷으로 전송한다.
MouseUp : 마우스가 왼쪽 클릭 상태에서 뗄 때만 'SendClickPacket'와 'SendPositionPacket'로 마우스의 클릭 여부와 위치 정보를 패킷으로 전송한다.
코드 보기
private void panel_Drawing_MouseMove(object sender, MouseEventArgs e)
{
Graphics g = this.panel_Drawing.CreateGraphics();
if (e.Button == MouseButtons.Left) // 왼클릭 상태일 때
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/move {id}, {e.Location}"); // 디버깅 출력용
//client_pen
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawLine(client_pen, playerLocation[id], e.Location);
SendPositionPacket(id, playerLocation[id].X, playerLocation[id].Y);
playerLocation[id] = e.Location;
}
}
}
private void panel_Drawing_MouseDown(object sender, MouseEventArgs e)
{
Console.WriteLine("MouseDown");
if (e.Button == MouseButtons.Left)
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/Down {id}, {e.Location}"); // 디버깅 출력용
playerLocation[id] = e.Location;
SendClickPacket(true, id, playerLocation[id].X, playerLocation[id].Y);
}
else
{
//Console.WriteLine($"else/Down {id}, {e.Location}"); //디버깅 출력용
playerLocation.Add(id, e.Location);
//player_Color.Add(id, Color.Black); // 기본색 추가
SendClickPacket(true, id, playerLocation[id].X, playerLocation[id].Y);
}
}
}
private void panel_Drawing_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
string id;
id = textBox_ID.Text; // id
if (playerLocation.ContainsKey(id))
{
//Console.WriteLine($"if/UP {id}, {e.Location}"); //디버깅 출력용
playerLocation[id] = e.Location;
SendPositionPacket(id, playerLocation[id].X, playerLocation[id].Y);
SendClickPacket(false, id, playerLocation[id].X, playerLocation[id].Y); // id
}
}
}
클라이언트의 ID에 따라 색 변경
Console.WriteLine(player_Color[cmd.ID]); : 스레드에서 받아서 클라이언트에 출력할 때 색값 디버깅 부분
Color를 Color형으로 전송하고 변환하였을 때 출력하여 디버깅을 진행하였는데 Color가 제대로 변환되지 않는 문제가 있었다.
Color를 Color형으로 JsonSerializer.Serialize하고 JsonSerializer.Deserialize했을 때 제대로 변환이 되지 않는 문제였다.
변환하는 방법을 찾던 와중 Color형이 다른 형으로 변환될 것 같은 메서드를 찾을 수 있었고,
그렇다면 Color형이 아닌 int나 다른 형태로 변환되어 진다면..? 변환된 형태를 JsonSerializer.Serialize하고,
이 데이터를 전송하고 받은 후에 JsonSerializer.Deserialize를 하고
다시 Color형으로 바꾸는 방법을 생각하였다.
여러 메서드가 있었지만 나는 int형으로 되어진 ToOle를 먼저 해보았고 나머지는 해보진 않았다.
https://learn.microsoft.com/ko-kr/dotnet/api/system.drawing.colortranslator.toole?view=net-7.0
ColorDialog에서 선택한 Color를 ColorTranslator.ToOle하여 ToOle형태로 변환하여 int형으로 저장한다
이 Color정보를 ColorPacket colp에 저장한 후 JsonSerializer.Serialize한다.
코드 보기
private void toolStripButton_Color_Click(object sender, EventArgs e)
{
ClickPacket cp = new ClickPacket();
ColorDialog cd = new ColorDialog();
if (cd.ShowDialog() == DialogResult.OK)
{
Color color = cd.Color;
client_pen = new Pen(color, dr);
int oleColor = ColorTranslator.ToOle(color);
ColorPacket colp = new ColorPacket();
colp.CMD = 'R';
string id;
id = textBox_ID.Text; // id
colp.ID = id;
if (player_int.ContainsKey(id))
{
colp.COLOR = oleColor;
player_int[id] = oleColor;
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
else
{
colp.COLOR = oleColor;
player_int.Add(id, oleColor);
string data = JsonSerializer.Serialize(colp);
sw.WriteLine(data);
sw.Flush();
}
}
}
클라이언트 멀티스레드
서버에서 데이터를 받아오고 json문자열을 원래의 객체로 복원한다. 일단 모든 패킷 클래스의 부모 클래스로 변환하여 명령의 종류(CMD)를 확인한 후 어떤 자식 클래스로 변환할 지 결정한다.
먼저 if (data.Contains('|'))으로 제시어를 따로받는 예외처리를 해주었는데, 앞서 서버에서 StringFormat을 '|'으로 해준 이유이다. 여기서 따로 StringFormat을 하지않고 받게된다면, 제시어와 다른 각종 정보(마우스 위치 정보, 색 정보 등)과 구분하지 못 하기 때문에 오류가 발생했었다. 그래서 '|'를 포함한 데이터를 갖고있다면 -> 제시어가 담긴 데이터 정보이며
해당 데이터를 '|'으로 힌트, 글자수, 제시어 구분해 두었기 때문에 세가지를 따로 Split하여 담을 수 있었다.
data == null일때 예외처리를 하지않으면 null오류가 발생하기 때문에 null에 대한 예외처리도 해주었다.
이후에는 CMD의 명령에 따라 case문으로 나뉘어 마우스 클릭 데이터, 마우스 위치 데이터, 펜 색 변환 데이터, 채팅 데이터를 구분하여 변환한다.
case 'C'에서 else의 처리를 ID또는 색이 없을때 예외처리를 하려 했으나 ID또는 색이 없을때 다른 클라이언트에서 그림을 그리게 되면 그림을 그리며 선이 그려질 때마다 메시지박스가 생성되어 예외처리를 하지 못했다.
casr 'R' Color부분에서는 FromOle를 통하여 변환된 Json데이터를 온전히 받아 올 수 있었고 색 변환 기능도 구현이 될 수 있었다.
코드 보기
void ThreadRecv()
{
while (this.isRunRecv)
{
try
{
string data = sr.ReadLine();
// DB 제시어만 따로 받아주는 if문
if (data.Contains('|'))
{
string[] rdata = data.Split('|');
hint = rdata[0];
Long = rdata[1];
word = rdata[2];
textBox1.Text = word;
textBox2.Text = hint;
textBox3.Text = Long;
}
if (data == null)
{
this.isRunRecv = false;
}
else
{
// json문자열을 원래의 객체로 복원
CmdPacket cmd = JsonSerializer.Deserialize<CmdPacket>(data);
switch (cmd.CMD)
{
// 명령을 확인한 후 어떤 클래스로 변환할 지 결정함
case 'P':
PositionPacket pp = JsonSerializer.Deserialize<PositionPacket>(data);
ColorPacket colp = JsonSerializer.Deserialize<ColorPacket>(data);
Graphics g = panel_Drawing.CreateGraphics();
g.SmoothingMode = SmoothingMode.AntiAlias;
if (playerLocation.ContainsKey(textBox_ID.Text))
{
//server_color = ColorTranslator.FromOle(colp.COLOR);
if (player_Color.ContainsKey(cmd.ID) == false)
{
g.DrawLine(Pens.Black, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
continue;
}
else if (player_Color[cmd.ID] == Color.White)
{
Pen erase_pen = new Pen(Color.White, 20);
g.DrawLine(erase_pen, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
continue;
}
//Console.WriteLine(player_Color[cmd.ID]);
server_pen = new Pen(player_Color[cmd.ID]);
// Console.WriteLine($"P if {player_Color[cmd.ID]}");
g.DrawLine(server_pen, playerLocation_End[cmd.ID].X,
playerLocation_End[cmd.ID].Y, pp.X, pp.Y);
playerLocation_End[cmd.ID] = new Point(pp.X, pp.Y);
}
break;
case 'C':
ClickPacket cp = JsonSerializer.Deserialize<ClickPacket>(data);
if (playerLocation.ContainsKey(textBox_ID.Text))
{
playerLocation_End[cmd.ID] = new Point(cp.X, cp.Y);
}
//else
//{
// MessageBox.Show("색또는 아이디를 지정해주세요");
//}
break;
case 'R':
colp = JsonSerializer.Deserialize<ColorPacket>(data);
if (player_Color.ContainsKey(cmd.ID))
{
player_Color[cmd.ID] = ColorTranslator.FromOle(colp.COLOR);
Console.WriteLine(player_Color[cmd.ID]);
if (player_Color[cmd.ID] == Color.Empty)
{
Console.WriteLine($"R if 색이 빈 값입니다.");
server_pen = new Pen(Color.Black); //검은색 기본값
}
else
{
Console.WriteLine($"R else {player_Color[cmd.ID]}");
server_pen = new Pen(player_Color[cmd.ID]);
//pen = new Pen(colp.COLOR, dr);
}
}
else
{
Color Trans_Color = ColorTranslator.FromOle(colp.COLOR);
player_Color.Add(cmd.ID, Trans_Color);
}
break;
case 'T':
ChatPacket chatp = JsonSerializer.Deserialize<ChatPacket>(data);
//AddMsgLogListBox($"수신 >> {chatp.CHATDATA}");
AddMsgLogListBox($"{chatp.ID} >> {chatp.CHATDATA}");
break;
}
}
}
catch (JsonException ex)
{
AddMsgLogListBox($"Json Error : {ex.Message}");
//AddMsgLogListBox($"Json Data : {data}");
}
catch (Exception ex)
{
AddMsgLogListBox($"Recv Error : {ex.Message}");
// this.isRunRecv = false;
}
}
Invoke(stopButtonState, null);
}