몇 달만에 올리는 포스트가 프로그래밍 입문절차인 별찍기입니다,,
파이썬은 학부 1학년때만 잠깐써보고 거의 방치해놔서리
기억을 더듬으면서 별찍기를 해봤습니다. 피라미드 모양으루다가
Type Hint / DocString / Generator 참고용 쌩기초입니다 ㅎ
실행화면은..
감사합니다
유니티 다룰 때 코루틴때문에 머리좀 아팠는데
제네레이터가 딱 그모양이네요 허허
몇 달만에 올리는 포스트가 프로그래밍 입문절차인 별찍기입니다,,
파이썬은 학부 1학년때만 잠깐써보고 거의 방치해놔서리
기억을 더듬으면서 별찍기를 해봤습니다. 피라미드 모양으루다가
Type Hint / DocString / Generator 참고용 쌩기초입니다 ㅎ
실행화면은..
감사합니다
유니티 다룰 때 코루틴때문에 머리좀 아팠는데
제네레이터가 딱 그모양이네요 허허
짧게 설명드리자면
클라이언트는 웹브라우저가 해줄거라 따로 필요없고
서버는 소켓생성 -> bind -> listen -> accept의 과정을 거치며
accepting loop에서 클라이언트의 요청이 들어오면 그에 맞게 response를 보냅니다.
몇년 전에 학교 과제용으로 만든거라 그냥 참고용으로만 봐주세요,,
우분투(64bit)에서 만들고 실행했었습니다.
따로 프로세스나 쓰레드를 생성하진 않았어요.
각 부분별로 설명을 드리자면..
헤더파일 선언 및 listen함수에 들어갈 BACKLOG값, 버퍼사이즈, 파일 이름 크기 정의입니다.
클라이언트가 파일을 요청하면 서버는 파일의 내용을 읽어와 보내주는데, 웹 브라우저가 이를 띄우기 위해선 앞에 html형식의 헤더를 붙여야합니다. 헤더가 없으면 제대로 사진 등을 출력하지 않아요.
어차피 앞부분은 다 똑같으니 image/jpeg , image/gif, audio/mp3, application/pdf, text/html만 따로 구분해서 만들고
앞부분도 따로 하면 메모리를 조금 덜 먹을 수 있겠습니다...
defaulthtml은 서버에 없는 파일을 요청했을 경우 보내는 html 메세지입니다.
실행파일 이름이 server인 경우 $./server [PORT번호] 로 실행하게 되는데, 포트번호를 입력하지 않은 경우 프로그램을 종료합니다.
소켓 파일디스크립터를 만듭니다.
서버의 IP, 포트번호를 설정하고
그를 이용해 소켓을 bind해줍니다.
listen함수입니다. BACKLOG값은 대기중인 큐에 넣을 최대 연결 수 라고 하는데 자세한건 더 찾아보시면 좋을것 같네요
이제 아래는 accept loop입니다.
클라이언트의 주소를 받고
콘솔창에 클라이언트의 주소를 띄워줍니다.
클라이언트가 보낸 값을 읽어옵니다.
클라이언트가 보낸 값중 GET / 뒷부분의 위치를 받고(5는 "GET /"의 length입니다)
띄어쓰기 전까지의 파일이름을 받고 뒷부분을 \0으로 채웁니다.
현재 작업중인 디렉토리에서 파일을 찾고 없으면 404 not found 메세지를 보냅니다.(defaulthtml)
파일이 있으면 확장자별로 헤더를 덧붙이고 전송합니다.
지금보니까 코딩 겁나 드럽게했네요..
암튼 서버 실행화면은 이렇게 생겼구요
포트번호 9999번으로 했을 경우
http://127.0.0.1:9999/testgif.gif 를 주소창에 치면
대충 요런식으로 파일을 읽어옵니다.
mp3,pdf,jpeg 등 파일을 열 수 있습니다.
봐주셔서 감사합니다.
다시 말씀드리지만 하도 오래된 코드라 참고용으로만 봐주셨으면 좋겠습니다..
아래는 복사가능한 전체코드입니다.
#include <stdio.h>
#include <arpa/inet.h> // GET IP ADDRESS IN 64 BIT MACHINE
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define BACKLOG 10
#define BUF_SIZE 1024
#define FILE_NAME 100
//FILE HEADER(before sending file, server send this header to client)
char imageheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: image/jpeg\r\n\r\n";
char gifheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: image/gif\r\n\r\n";
char mp3header[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: audio/mp3\r\n\r\n";
char pdfheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: application/pdf\r\n\r\n";
char htmlheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: text/html\r\n\r\n";
char defaulthtml[] = "HTTP/1.1 404 Not Found\r\n"
"Content-Type : text/html; charset=UTF-8\r\n\r\n"
"<!DOCTYPE html>\r\n"
"<html><head><title>WELCOM TO MY SERVER</title>\r\n"
"<body>I've got wrong file name. check it please.</body>"
"</html>";
int main(int argc, char *argv[])
{
int sockfd, new_fd; // sockfd : server socket fd, new_fd : client socket fd
struct sockaddr_in my_addr; // server addr
struct sockaddr_in their_addr; // client addr
char buffer[BUF_SIZE];
char *strptr1;
int count=0;
int n; // reading variable
int sin_size; // client addr size
int filesize;
int PORT = atoi(argv[1]);
if(argc<2){
printf("error (no port)\n");
exit(0);
} // if there is no port #, terminate program.
///////////////////MAKE SOCKET////////////////////
if((sockfd=socket(PF_INET, SOCK_STREAM, 0))==-1)
{
perror("socket error");
exit(0);
}
///////////////////BIND SOCKET////////////////////
memset(&my_addr,0,sizeof(my_addr)); // clear by 0
my_addr.sin_family = AF_INET; // AF_INET in IPv4
my_addr.sin_port = htons(PORT); // port # , short type data(PORT)
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 32bit IPv4 addr , long type data(IP)
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))==-1)
{
perror("bind error");
exit(0);
}
//////////////////////LISTEN//////////////////////
if(listen(sockfd, BACKLOG)==-1)
{
perror("listen error");
exit(0);
}
//////////////////////////////////////////////////
sin_size = sizeof(struct sockaddr_in);
////////////////////ACCEPTING/////////////////////
while(1){ // accepting loop
if((new_fd=accept(sockfd, (struct sockaddr*)&their_addr, &sin_size))==-1)
{
perror("fail to accepting");
continue;
}
//show client's ip address
printf("server : got connection from %s\n", inet_ntoa(their_addr.sin_addr));
//read request message and print it
bzero(buffer,BUF_SIZE);
n=read(new_fd,buffer,BUF_SIZE);
if(n<0) perror("READING ERROR");
printf("%s", buffer);
///////FIND FILENAME///////
strptr1 = strstr(buffer,"GET /");
strptr1 += 5;
char filename[100];
char pwd[BUF_SIZE];
memset(filename,0,100);
int i=0;
int fd;
while(1)
{
if(strptr1[i]==' ')
{
filename[i] = '\0'; break;
}
filename[i]=strptr1[i];
i++;
}//get filename until meet the ' ' character.
/////FIND FILE IN WORKING DIRECTORY/////
getcwd(pwd,BUF_SIZE);
strcat(pwd,"/");
strcat(pwd,filename);
char fbuf[BUF_SIZE];
/////FILE is not in working directory/////
if((fd=open(pwd,O_RDONLY))==-1)
{
write(new_fd,defaulthtml,sizeof(defaulthtml)-1);
printf("Server sent '404 not found message' to client\n");
}
/////FILE is in working directory/////
////////SENDING FILE TO CLIENT////////
else
{ //SEND FILE HEADER//
if(strstr(filename,".jpg")!=NULL||strstr(filename,".jpeg")!=NULL)
write(new_fd,imageheader,sizeof(imageheader)-1);
else if(strstr(filename,".gif")!=NULL)
write(new_fd,gifheader,sizeof(gifheader)-1);
else if(strstr(filename,".pdf")!=NULL)
write(new_fd,pdfheader,sizeof(pdfheader)-1);
else if(strstr(filename,".mp3")!=NULL)
write(new_fd,mp3header,sizeof(mp3header)-1);
else if(strstr(filename,".html")!=NULL)
write(new_fd,htmlheader,sizeof(htmlheader)-1);
//SEND FILE//
while((n=read(fd,fbuf,BUF_SIZE))>0)
write(new_fd,fbuf,n);
printf("Server sent file completly : %s\n\n", filename);
}
close(new_fd);
close(fd);
}
}
서버 폼에서 /open 명령어로 서버를 엽니다.
서버가 열렸습니다.
클라이언트 폼입니다. 타 클라이언트의 접속 및 퇴장을 알 수 있습니다.
서버측 폼입니다. 클라이언트의 접속 및 퇴장을 알 수 있습니다.
+
채팅 화면은 위와 같습니다. 맨 위 클라이언트에서 Failed to Connect가 뜬 이유는 아이피 입력시 오타가 나서..
같은 공유기 내에 아이피로 진행했습니다. (데스크탑-서버, 노트북-클라이언트)
봐주셔서 감사합니다. 격한 훈수 환영합니다.
깃허브
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 2. 클라이언트 (2) | 2020.03.27 |
---|---|
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 1. 서버 (0) | 2020.03.27 |
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 0. 준비 (0) | 2020.03.27 |
ID 입력창입니다.
ID 입력 후 버튼 클릭 또는 키보드 엔터키 입력시 현재 창을 숨기고 클라이언트 폼을 띄웁니다.
클라이언트 폼입니다. 서버폼이랑 별 다를건 없습니다.
0. 전체 폼
FormClosing시 Disconnect() 메소드를 실행하게끔 합니다.
1. 채팅로그(RichTextBox, Name : ChatLog)
채팅 로그를 띄워줍니다. 서버 콘솔창과 마찬가지로 RichTextBox입니다.
2. 채팅 참가자 목록(ListBox, Name : ChatterList)
채팅 참가자 목록을 띄웁니다.
3. 대화 입력창(TextBox, Name : InputField)
일반 TextBox입니다. 키보드 엔터키 입력시 마찬가지로 전송되게끔 합니다.
이를 위해 KeyDown시 InputField_KeyDown() 메소드를 실행케 합니다.
4. 전송 버튼(Button, Name : EnterBtn)
클릭시 EnterBtn_Click() 메소드를 실행합니다.
5. 나가기 버튼(Button, Name : QuitBtn) // 나가는 명령어가 /exit라 ExitBtn으로 통일하는걸 깜빡했습니다..,
서버와 접속을 끊는 Disconnect() 메소드를 실행합니다.
ClientForm.cs 코드입니다.
Public CilentForm(string ID)
유저ID를 ID입력 폼에서 받아 폼을 생성합니다.
void Connect(string s)
InputField에서 /connect [클라이언트IP] [서버IP] [서버PORT] 입력시 서버와 연결합니다.
' '기준으로 문자열을 split해 IP를 얻어냅니다.
연결 실패시 메세지박스를 띄우며 적당히 예외처리를 추가하시면 되겠습니다.
void Display(string msg)
ChatLog에 메세지를 띄웁니다.
서버와 마찬가지로 CheckForIllegalCrossThreadCalls = false; 를 사용해 크로스 쓰레드 예외를 피합니다.
void Receiver()
서버로부터 오는 메세지를 받는 메소드입니다. 컨트롤 메세지일 경우 ChatLog에 띄우지 않고 따로 처리합니다.
bool isControlMsg(string msg)
컨트롤 메세지인 경우 그에 맞는 처리를 하고 ChatLog에 띄우지 않기 위해 bool형 변수를 반환합니다.
void Sender(string s)
서버로 메세지를 보내는 메소드입니다. 사실 Disconnect() 메소드의 전송부분도 Sender로 대체하면 되긴합니다. 새벽에 만드느라 살짝 엉성한 부분이 많네요
void Controller(string s)
InputField에서 메세지를 받아 컨트롤 메세지라면 맞는 처리를, 그렇지 않으면 서버에 그대로 전송합니다.
void Disconnet()
폼을 닫거나 나가기 버튼을 누를 때 서버와 연결을 끊습니다.
void InputField_Keydown(object sender, KeyEventargs e)
키보드의 엔터키 입력시 EnterBtn_Click()메소드를 실행합니다.
void EnterBtn_Click(object sender, Eventargs e)
InputField의 텍스트를 비우고 Controller(해당 텍스트) 메소드를 실행합니다.
후에 InputField에 다시 포커스를 맞춥니다.
void RefreshUserlist()와 void ClientForm_Load()는 사용하지않았습니다.
예외가 일어나는 부분이 꽤 많습니다... VS 디버그 모드로 해당부분 예외 발견 후 처리하는 구문을 많이 쓰셔야 할 수 있습니다.
복사가능한 전체 코드는 아래에 있습니다.
EnterID.cs
using System;
using System.Windows.Forms;
namespace myClient
{
public partial class EnterID : Form
{
string ID;
public EnterID()
{
InitializeComponent();
}
private void IDbtn_Click(object sender, EventArgs e)
{
if(IDInputField.Text.Equals(string.Empty))
{
}
else
{
ID = IDInputField.Text;
ClientForm clientform = new ClientForm(ID);
this.Hide();
clientform.ShowDialog();
}
}
private void IDInputField_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Return) IDbtn_Click(sender, e);
}
}
}
ClientForm.cs
using System;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace myClient
{
public partial class ClientForm : Form
{
string ID;
NetworkStream stream = default(NetworkStream);
TcpClient client = new TcpClient();
public ClientForm(string ID)
{
this.ID = ID;
InitializeComponent();
}
void Connect(string s)
{
char split = ' ';
string[] parsedIP = s.Split(split); // 1 : client, 2 : server, 3 : server Port
try
{
IPEndPoint clientAddr = new IPEndPoint(IPAddress.Parse(parsedIP[1]), 0);
IPEndPoint serverAddr = new IPEndPoint(IPAddress.Parse(parsedIP[2]), Int32.Parse(parsedIP[3]));
client = new TcpClient(clientAddr);
client.Connect(serverAddr);
stream = client.GetStream();
Display("Chat Server Connected...");
}
catch(Exception e)
{
Display("Failed to Connect...");
}
if(!client.Connected)
{
MessageBox.Show("usage : /connect 클라이언트IP 서버IP 서버PORT");
InputField.Text = string.Empty;
InputField.Focus();
}
else
{
byte[] buffer = Encoding.Default.GetBytes(ID);
stream.Write(buffer, 0, buffer.Length);
Thread clientThread = new Thread(Receiver);
clientThread.IsBackground = true;
clientThread.Start();
}
}
void Display(string msg)
{
CheckForIllegalCrossThreadCalls = false;
ChatLog.AppendText(msg+"\r\n");
ChatLog.Select(ChatLog.Text.Length, 0);
ChatLog.ScrollToCaret();
}
void Receiver()
{
try
{
while (true)
{
int bufferSize = client.ReceiveBufferSize;
byte[] buffer = new byte[bufferSize];
int bytes = stream.Read(buffer, 0, buffer.Length);
string msg = Encoding.Default.GetString(buffer, 0, bytes);
if(!isControlMsg(msg)) Display(msg);
}
}
catch(IOException e)
{
//
}
}
bool isControlMsg(string msg)
{
bool isCtrl = true;
if (msg.StartsWith("**userlist** "))
{
ChatterList.Items.Clear();
char split = ' ';
string[] userlist = msg.Split(split);
foreach(string s in userlist)
{
if (s.StartsWith("**")) continue;
ChatterList.Items.Add(s);
}
}
else isCtrl = false;
return isCtrl;
}
void RefreshUserlist(string[] ul)
{
}
void Sender(string s)
{
byte[] buffer = Encoding.Default.GetBytes(s);
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
}
void Controller(string s)
{
if(s.StartsWith("/connect "))
{
Connect(s);
}
else if(!client.Connected)
{
MessageBox.Show("서버에 먼저 연결해주세요.");
InputField.Text = string.Empty;
InputField.Focus();
}
else if(s.Equals("/exit"))
{
Disconnect();
}
else
{
Sender(s);
}
}
void Disconnect()
{
byte[] buffer = Encoding.Default.GetBytes("/exit");
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
stream.Close();
client.Close();
Application.ExitThread();
this.Close();
}
void Disconnect(object sender, EventArgs e)
{
byte[] buffer = Encoding.Default.GetBytes("/exit");
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
stream.Close();
client.Close();
Application.ExitThread();
this.Close();
}
private void InputField_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Return) EnterBtn_Click(sender, e);
}
private void EnterBtn_Click(object sender, EventArgs e)
{
string input = InputField.Text;
Controller(input);
InputField.Text = string.Empty;
InputField.Focus();
}
private void ClientForm_Load(object sender, EventArgs e)
{
}
}
}
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 3. 실행 화면 (4) | 2020.03.27 |
---|---|
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 1. 서버 (0) | 2020.03.27 |
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 0. 준비 (0) | 2020.03.27 |
서버쪽 창입니다. 서버에서 여러가지 컨트롤을 할 수 있게끔 입력창을 만들었고, 서버의 상태를 확인할 수 있게끔 콘솔창을 넣었습니다. 오른쪽 리스트박스는 채팅 참가자 리스트를 실시간으로 반영합니다.
0. 전체 폼
취향에 맞게 꾸미셔도 됩니다. Load시 ChatServerForm_Load() 메소드를 실행하게끔 합니다.
1. 콘솔창(RichTextBox, name : myConsole)
콘솔창을 RichTextBox로 만든 이유는 너무 길어지면 스크롤바를 추가하게끔 하기 위해서입니다. 코드 내 이름은 myConsole이고, multiline = true, readonly = true로 하시면 되겠습니다. 크기는 취향껏
2. 채팅참가자 목록(ListBox, name : ClientList)
굳이 실시간으로 반영하지 않고 명령어로 물어볼 때만 출력하게끔 할까 하다가 그냥 하는김에 실시간으로 반영하고 클라이언트들에게도 전송하게끔 했습니다. 그 과정에 쓰레드를 하나 더 생성해버렸지만
3. 입력창(TextBox, name : ControlInput)
여러 명령어들을 입력할 입력창입니다. 굳이 다른 옵션 건드릴 필요가 없습니다.
KeyDown시 ControlInput_KeyDown() 메소드를 실행하게끔 추가합니다.
4. 전송버튼(Button, name : Enter)
전송버튼입니다. 키보드의 엔터키를 눌러도 작동하게끔 만들었습니다.
Click시 ControlEnter() 메소드를 실행합니다.
ChatServerForm.cs 코드는 아래와 같습니다.
메소드 하나하나 살펴보자면
Addtext(string text)
myConsole에 text를 이어붙입니다. AppendText 아래엔 스크롤바를 자동으로 아래로 내리게끔 하는 기능입니다.
ControlEnter(object sender, EventArgs e)
Enter버튼 입력시 실행되는 메소드입니다. ControlInput 텍스트박스에 입력한 문자열을 Controller 메소드로 옮겨 실행하고 텍스트박스를 비운 후 포커스를 다시 텍스트박스로 옮깁니다.
RefreshChatters()
채팅 참가자 리스트를 리스트박스에 실시간으로 업데이트합니다. (1초간격)
bufferedList는 각 클라이언트에 참가자 리스트를 전송하기 위해 만들었습니다. 앞에 컨트롤 메시지라는 "**userlist** "를 동봉했습니다. 후에 MyChatServer 클래스에서 이를 전송합니다.
CheckForIllegalCrossThreadCalls = false; 를 추가해 크로스 스레드 예외를 무시했습니다. 어차피 이 자원을 사용하는 스레드는 하나밖에 없습니다.
OpenServer(object s)
/open [ServerIP] 명령어 입력시 실행됩니다. 문자열s로부터 "xxx.xxx.xxx.xxx"형태의 IP주소를 얻고 이를 IPAddress.Parse()를 통해 IP주소값으로 변환, 서버를 엽니다. 포트번호는 제 맘대로 넣었습니다...
client = server.AcceptTcpClient() 를 통해 클라이언트를 계속 받고, 받을 때마다 해당 클라이언트를 처리하는 서버 쓰레드를 만들어 Listen() 메소드를 실행합니다.
이때 서버 쓰레드에 해당 클라이언트의 TcpClient와 ID를 넘겨주며, 폼을 컨트롤 할 수 있게끔 this도 전달해줍니다.
예외 발생시 break;라 했지만 보다 섬세한 예외처리를 하시는게 더 좋을거같습니다.
vold CloseServer()
구현은 안했지만 /close 입력시 실행되는 메소드입니다. 서버를 닫아주고 그에 따른 처리를 하시면 됩니다.
Controller(string s)
ControlEnter 메소드에서 문자열을 받아 처리하는 메소드입니다.
본문에선 지웠지만 금지어를 추가하는
else if (s.StartsWith("/spam "))
{
string spam = s.Substring("/spam ".Length);
if (spams.Contains(spam))
{
AddText(string.Format("[Server] Spam {0} is already banned.\r\n", spam));
}
else
{
spams.Add(spam);
AddText(string.Format("[Server] Spam {0} Added.\r\n", spam));
}
}
등의 조건문이 있었습니다. 취향껏 여러 조건문을 덧붙이셔도 됩니다.
ControllInput_KeyDown(object sender, KeyEventArgs e)
키보드의 엔터키(Keys.Return) 입력 시에도 ControlEnter() 메소드를 호출합니다.
ChatServerForm_Load(object sender, EventArgs e)
폼 생성과 동시에 채팅 참가자 리스트를 갱신하는 쓰레드를 만들어 실행했습니다.
각 클라이언트와 통신을 담당할 MyChatServer.cs 코드입니다.
public MyChatServer(client, userID, serverForm)
이 객체는 생성자로 TcpClient, userID, serverForm을 받습니다.
생성시 서버 콘솔에 해당 user의 접속사실을 띄우고 모든 유저에게 접속을 알립니다.
그리고 쓰레드를 생성해 Refresh() 메소드를 실행시킵니다.
Refresh()
이 메소드는 1초 간격으로 클라이언트에게 채팅 참가자 리스트를 "**userlist** user1 user2 ..." 형식으로 전송합니다.
클라이언트는 이 메세지를 받으면 유저들을 본인들의 ListBox에 대입시킵니다.
Broadcast(string msg, string ID)
모든 유저에게 메세지를 전달합니다. [ID] 메세지 형식입니다. 공지를 하고싶은 경우 ID에 "Notice"를 넣었습니다.
bool Controller(string str)
클라이언트에게 받은 메세지가 채팅이 아닌 컨트롤 메세지임을 판단합니다. "/exit"나 본문엔 없지만 "/spamlist" 등으로 금지어 목록을 요청하는 경우 해당 메세지를 Broadcast하는게 아닌 서버에서 따로 메세지를 만들어 해당 유저에게 보내도록 합니다.
void Listen()
계속해서 클라이언트에게 오는 메세지를 받습니다. 연결상태를 확인해 연결중이 아니라면 종료합니다.
마찬가지로 취향껏 예외처리를 하시면 되겠습니다.
아래는 복사 가능한 전체 코드입니다.
MyChatServer.cs
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace chattingServer
{
class MyChatServer
{
static Dictionary<string, TcpClient> userData = new Dictionary<string, TcpClient>();
TcpClient client;
NetworkStream stream = default (NetworkStream);
string userID;
ChatServerForm serverForm;
public MyChatServer(TcpClient client, string userID, ChatServerForm serverForm)
{
this.userID = userID;
this.client = client;
this.serverForm = serverForm;
userData.Add(userID, client);
serverForm.AddText(string.Format($"[{userID}] connected to server.\r\n"));
Broadcast(string.Format("{0} joined to server", userID), "Notice");
Thread refresher = new Thread(Refresh);
refresher.IsBackground = true;
refresher.Start();
}
void Refresh()
{
while(true)
{
if (!client.Connected) break;
byte[] bytes = Encoding.Default.GetBytes(serverForm.bufferedList);
stream.Write(bytes, 0, bytes.Length);
stream.Flush();
Thread.Sleep(1000);
}
}
void Broadcast(string msg, string ID)
{
foreach (var user in userData)
{
TcpClient client = user.Value as TcpClient;
stream = client.GetStream();
byte[] buffer = Encoding.Default.GetBytes(string.Format("[{0}] {1}", ID, msg));
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
}
}
bool Controller(string str)
{
bool isControlMsg = true;
if (str.Equals("/exit"))
{
serverForm.users.Remove(userID);
Broadcast(string.Format($"{userID} exited"), "Notice");
userData.Remove(userID);
}
else
{
isControlMsg = false;
}
return isControlMsg;
}
public void Listen()
{
NetworkStream stream = client.GetStream();
try
{
while (true)
{
int bufLength;
byte[] buffer = new byte[1024];
string str = string.Empty;
if (!client.Connected) break;
try
{
while ((bufLength = stream.Read(buffer, 0, buffer.Length)) != 0)
{
str = Encoding.Default.GetString(buffer, 0, bufLength);
serverForm.AddText(string.Format($"[{userID}] {str}\r\n"));
if (!Controller(str)) Broadcast(str, userID);
}
}
catch (IOException e)
{
}
}
}
catch (SocketException e)
{
serverForm.AddText(string.Format("{0} 클라이언트 오류로 인한 퇴장\r\n", userID));
stream.Close();
client.Close();
}
finally
{
serverForm.AddText(string.Format("{0} 클라이언트 퇴장\r\n", userID));
stream.Close();
client.Close();
}
}
}
}
ChatServerForm.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
namespace chattingServer
{
public partial class ChatServerForm : Form
{
public string bufferedList;
public List users = new List();
TcpListener server;
public void AddText(string text)
{
myConsole.AppendText(text);
myConsole.Select(myConsole.Text.Length, 0);
myConsole.ScrollToCaret();
}
void ControlEnter(object sender, EventArgs e)
{
string input = ControlInput.Text;
Controller(input);
ControlInput.Text = string.Empty;
ControlInput.Focus();
}
void RefreshChatters()
{
CheckForIllegalCrossThreadCalls = false;
while (true)
{
bufferedList = "**userlist** ";
ClientList.Items.Clear();
foreach(string user in users)
{
ClientList.Items.Add(user);
bufferedList += (user+" ");
}
Thread.Sleep(1000);
}
}
void OpenServer(object s)
{
string serverIP = s.ToString().Substring("/open ".Length);
TcpClient client = new TcpClient();
const int Port = 10203;
IPEndPoint serverAddr = new IPEndPoint(IPAddress.Parse(serverIP), Port);
server = new TcpListener(serverAddr);
try
{
server.Start();
}
catch (SocketException e)
{
MessageBox.Show("올바르지 않은 주소입니다.");
return;
}
AddText(String.Format("Server Opened. [{0}]\r\n", serverAddr.ToString()));
while (true)
{
try
{
client = server.AcceptTcpClient();
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytes = stream.Read(buffer, 0, buffer.Length);
string userID = Encoding.Default.GetString(buffer, 0, bytes);
users.Add(userID);
MyChatServer mychat = new MyChatServer(client, userID, this);
Thread serverThread = new Thread(new ThreadStart(mychat.Listen));
serverThread.IsBackground = true;
serverThread.Start();
}
catch (Exception e) { break; }
}
client.Close();
server.Stop();
}
void CloseServer()
{
// 서버 닫는 처리
}
void Controller(string s)
{
if (s.Equals(string.Empty)) return;
else if(s.Equals("/close"))
{
CloseServer();
AddText("[Server] Closed.\r\n");
}
else if (s.StartsWith("/open "))
{
Thread open = new Thread(OpenServer);
open.IsBackground = true;
open.Start(s);
}
}
public ChatServerForm()
{
InitializeComponent();
}
private void ControlInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Return) ControlEnter(sender, e);
}
private void ChatServerForm_Load(object sender, EventArgs e)
{
Thread refresher = new Thread(RefreshChatters);
refresher.IsBackground = true;
refresher.Start();
}
}
}
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 3. 실행 화면 (4) | 2020.03.27 |
---|---|
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 2. 클라이언트 (2) | 2020.03.27 |
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 0. 준비 (0) | 2020.03.27 |
C# 윈폼을 이용해서 1대N 채팅 서버 및 클라이언트를 만들겁니다.
대략적인 골격은 위 그림처럼 되겠습니다.
윈폼 클래스에 이것저것 다 때려박아봤습니다...
보시기에 지저분할 수도 있을거같네요.
실행 과정을 말씀드리자면
<서버>
1. 윈폼에서 /open [ServerIP] 명령어를 TextBox에 입력 후 Enter 버튼을 누르면 새 쓰레드를 만들어 서버를 엽니다. 포트는 적당히 설정해주시면 됩니다.
2. 서버는 while(true) { .... AcceptTcpClient(); ... } 를 통해 클라이언트를 계속 받습니다.
3. 클라이언트를 받을 때 마다 해당 클라이언트와 통신할 ChatServer 인스턴스를 만듭니다. 이 인스턴스는 생성시 서버로부터 TcpClient 데이터를 받고 Listen() { while(true} { ... read(); ... } } 를 통해 실시간 클라이언트로부터 데이터를 읽습니다.
<클라이언트>
1. ID를 입력하면 Client 폼을 띄웁니다.
2. /connect [ClientIP] [ServerIP] [ServerPort] 를 입력해 서버와 연결합니다. (Client 쪽 포트는 OS에서 알아서 처리하게끔 합니다)
3. 클라이언트가 메세지 입력 후 전송버튼을 누르면 NetworkStream.write(); 메소드로 서버측에 메세지를 전송합니다.
4. 백그라운드로 Receiver() 메소드를 담당할 쓰레드를 만들어 서버쪽에서 오는 메세지를 받습니다.
주요 기능만 구현하고 부가적인 기능(예를 들자면 서버에 연결 안된채로 클라이언트 폼을 닫을때 처리라거나..여러 예외처리들)은 생략했습니다. 이것들은 다음 글에 천천히 쓰겠습니다 허허
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 3. 실행 화면 (4) | 2020.03.27 |
---|---|
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 2. 클라이언트 (2) | 2020.03.27 |
C# TCP - 1:N 윈폼 채팅 서버/클라이언트 - 1. 서버 (0) | 2020.03.27 |