네트워크 통신에서 소켓(Socket)은 서로 다른 호스트에 있는 애플리케이션 프로세스 간의 양방향 통신을 위한 추상적인 종단점을 의미합니다. 소켓은 네트워크를 통해 데이터를 교환하는 애플리케이션 계층 프로세스를 위한 메커니즘을 제공하며, 애플리케이션과 네트워크 프로토콜 스택 사이의 인터페이스 역할을 합니다.
소켓 통신을 이용하는 가장 일반적인 시나리오는 클라이언트-서버 모델입니다. 기본적인 통신 흐름은 다음과 같습니다.
- **서버 시작**: 서버는 특정 IP 주소와 포트 번호에 소켓을 바인딩하고, 클라이언트 연결 요청을 기다립니다.
- **클라이언트 연결**: 클라이언트는 서버의 IP 주소와 포트 번호를 사용하여 서버에 연결을 시도합니다.
- **연결 수락**: 서버는 클라이언트의 연결 요청을 수락하고, 각 클라이언트와의 통신을 위한 새로운 소켓을 생성합니다.
- **데이터 교환**: 연결이 수립되면 클라이언트와 서버는 서로 데이터를 주고받을 수 있습니다.
- **연결 종료**: 통신이 완료되면 양측은 소켓 연결을 종료합니다.
다음은 C# WinForms 환경에서 소켓을 활용하여 간단한 채팅 서버와 클라이언트를 구현하는 예제 코드입니다.
채팅 서버 구현
채팅 서버는 클라이언트의 연결을 수락하고, 메시지를 수신하며, 연결된 클라이언트들에게 메시지를 전송하는 역할을 합니다. 여러 클라이언트의 동시 접속을 처리하기 위해 각 클라이언트와의 통신을 별도의 스레드에서 처리합니다.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Linq;
namespace ChatServerApp
{
// 메시지 유형 정의 (공통 모듈)
public enum ChatMessageType : byte
{
TextMessage = 1,
ScreenShake = 2
}
// 연결된 클라이언트 정보
public class ConnectedClient
{
public string ClientIdentifier { get; set; }
public string EndpointAddress { get; set; }
public Socket ClientSessionSocket { get; set; }
}
// UI 표시용 클라이언트 정보
public class ClientDisplayItem
{
public string Id { get; set; }
public string Name { get; set; }
}
public partial class ServerMainForm : Form
{
private Socket _serverListenerSocket;
private List<ConnectedClient> _connectedClients = new List<ConnectedClient>();
public ServerMainForm()
{
InitializeComponent();
}
private void ServerMainForm_Load(object sender, EventArgs e)
{
// UI 스레드 충돌 방지 (디버깅 목적)
Control.CheckForIllegalCrossThreadCalls = false;
serverIpTextBox.Text = "127.0.0.1"; // 기본 IP
serverPortTextBox.Text = "50000"; // 기본 포트
}
private void startServerButton_Click(object sender, EventArgs e)
{
try
{
string ipAddr = serverIpTextBox.Text.Trim();
string portNum = serverPortTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(ipAddr) || string.IsNullOrWhiteSpace(portNum))
{
MessageBox.Show("IP 주소와 포트 번호를 입력하세요!");
return;
}
StartListeningForClients(IPAddress.Parse(ipAddr), int.Parse(portNum));
serverLogTextBox.AppendText("서버가 성공적으로 시작되었습니다...\r\n");
}
catch (Exception ex)
{
MessageBox.Show($"서버 시작 오류: {ex.Message}");
}
}
private void StartListeningForClients(IPAddress ipAddress, int port)
{
_serverListenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint localEndpoint = new IPEndPoint(ipAddress, port);
_serverListenerSocket.Bind(localEndpoint);
_serverListenerSocket.Listen(10); // 최대 10개의 대기열
Thread acceptThread = new Thread(AcceptNewConnections);
acceptThread.IsBackground = true;
acceptThread.Start(_serverListenerSocket);
}
private void AcceptNewConnections(object listenerSocketObj)
{
var listenerSocket = listenerSocketObj as Socket;
while (true)
{
Socket clientSocket = null;
string clientEndpointStr = string.Empty;
try
{
clientSocket = listenerSocket.Accept(); // 클라이언트 연결 수락
clientEndpointStr = clientSocket.RemoteEndPoint.ToString();
if (clientSocket.Connected)
{
serverLogTextBox.AppendText($"\r\n새로운 클라이언트 연결: {clientEndpointStr}");
var newClient = new ConnectedClient
{
ClientIdentifier = Guid.NewGuid().ToString(),
EndpointAddress = clientEndpointStr,
ClientSessionSocket = clientSocket
};
_connectedClients.Add(newClient);
UpdateClientListUI(); // UI 업데이트
// 각 클라이언트의 메시지를 수신하기 위한 새 스레드 시작
Thread receiveThread = new Thread(() => HandleClientMessages(clientSocket, clientEndpointStr));
receiveThread.IsBackground = true;
receiveThread.Start();
}
}
catch (Exception ex)
{
if (clientSocket != null)
{
RemoveClient(clientEndpointStr);
serverLogTextBox.AppendText($"\r\n클라이언트 연결 오류 또는 종료: {clientEndpointStr} - {ex.Message}");
}
// 서버 소켓이 닫히면 accept 루프 종료
if (_serverListenerSocket == null || !_serverListenerSocket.IsBound) break;
}
}
}
private void HandleClientMessages(Socket clientSocket, string clientEndpoint)
{
byte[] receiveBuffer = new byte[4 * 1024 * 1024]; // 4MB 버퍼
while (true)
{
try
{
int bytesRead = clientSocket.Receive(receiveBuffer);
if (bytesRead <= 0)
{
// 클라이언트 연결 종료
RemoveClient(clientEndpoint);
serverLogTextBox.AppendText($"\r\n클라이언트 연결 종료: {clientEndpoint}");
break;
}
// 메시지 프로토콜: 첫 번째 바이트는 메시지 타입, 나머지는 데이터
ChatMessageType msgType = (ChatMessageType)receiveBuffer[0];
string messageData = Encoding.UTF8.GetString(receiveBuffer, 1, bytesRead - 1);
serverLogTextBox.AppendText($"\r\n[{clientEndpoint}][타입: {msgType}]: {messageData}");
}
catch (SocketException sex)
{
// 클라이언트 소켓 오류 (연결 끊김 등)
RemoveClient(clientEndpoint);
serverLogTextBox.AppendText($"\r\n클라이언트 소켓 오류 및 연결 해제: {clientEndpoint} - {sex.Message}");
break;
}
catch (Exception ex)
{
serverLogTextBox.AppendText($"\r\n메시지 처리 중 오류 발생: {clientEndpoint} - {ex.Message}");
break;
}
}
clientSocket?.Close();
clientSocket?.Dispose();
}
private void RemoveClient(string endpoint)
{
var clientToRemove = _connectedClients.FirstOrDefault(c => c.EndpointAddress == endpoint);
if (clientToRemove != null)
{
_connectedClients.Remove(clientToRemove);
UpdateClientListUI();
}
}
private void UpdateClientListUI()
{
// UI 스레드에서 ListBox 업데이트
if (clientListBox.InvokeRequired)
{
clientListBox.Invoke(new MethodInvoker(delegate
{
clientListBox.DataSource = _connectedClients.Select(c => new ClientDisplayItem { Id = c.ClientIdentifier, Name = c.EndpointAddress }).ToList();
clientListBox.DisplayMember = "Name";
clientListBox.ValueMember = "Id";
}));
}
else
{
clientListBox.DataSource = _connectedClients.Select(c => new ClientDisplayItem { Id = c.ClientIdentifier, Name = c.EndpointAddress }).ToList();
clientListBox.DisplayMember = "Name";
clientListBox.ValueMember = "Id";
}
}
// 서버에서 특정 클라이언트에 메시지 전송
private void sendToSelectedButton_Click(object sender, EventArgs e)
{
string messageContent = serverSendTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(messageContent))
{
MessageBox.Show("보낼 메시지를 입력하세요.");
return;
}
if (clientListBox.SelectedItem == null)
{
MessageBox.Show("메시지를 보낼 클라이언트를 선택하세요.");
return;
}
var selectedClientItem = clientListBox.SelectedItem as ClientDisplayItem;
if (selectedClientItem == null) return;
var targetClient = _connectedClients.FirstOrDefault(c => c.ClientIdentifier == selectedClientItem.Id);
if (targetClient?.ClientSessionSocket != null)
{
try
{
byte[] packet = CreateMessagePacket(messageContent, ChatMessageType.TextMessage);
targetClient.ClientSessionSocket.Send(packet);
serverLogTextBox.AppendText($"\r\n[서버] -> [{targetClient.EndpointAddress}]: {messageContent}");
}
catch (Exception ex)
{
serverLogTextBox.AppendText($"\r\n메시지 전송 실패 ({targetClient.EndpointAddress}): {ex.Message}");
RemoveClient(targetClient.EndpointAddress);
}
}
}
// 서버에서 모든 클라이언트에 메시지 브로드캐스트
private void broadcastButton_Click(object sender, EventArgs e)
{
string messageContent = serverSendTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(messageContent))
{
MessageBox.Show("보낼 메시지를 입력하세요.");
return;
}
if (_connectedClients.Count == 0)
{
MessageBox.Show("현재 연결된 클라이언트가 없습니다.");
return;
}
byte[] packet = CreateMessagePacket(messageContent, ChatMessageType.TextMessage);
List<ConnectedClient> clientsToRemove = new List<ConnectedClient>();
foreach (var client in _connectedClients)
{
try
{
client.ClientSessionSocket?.Send(packet);
}
catch (Exception)
{
clientsToRemove.Add(client); // 전송 실패한 클라이언트 목록에 추가
}
}
foreach (var client in clientsToRemove)
{
RemoveClient(client.EndpointAddress);
serverLogTextBox.AppendText($"\r\n브로드캐스트 실패 및 연결 해제: {client.EndpointAddress}");
}
serverLogTextBox.AppendText($"\r\n[서버] -> [모두]: {messageContent}");
}
// 서버에서 특정 클라이언트에 화면 흔들기 요청 전송
private void sendShakeButton_Click(object sender, EventArgs e)
{
if (clientListBox.SelectedItem == null)
{
MessageBox.Show("화면을 흔들 클라이언트를 선택하세요.");
return;
}
var selectedClientItem = clientListBox.SelectedItem as ClientDisplayItem;
if (selectedClientItem == null) return;
var targetClient = _connectedClients.FirstOrDefault(c => c.ClientIdentifier == selectedClientItem.Id);
if (targetClient?.ClientSessionSocket != null)
{
try
{
byte[] packet = CreateMessagePacket("", ChatMessageType.ScreenShake); // 데이터는 비워둠
targetClient.ClientSessionSocket.Send(packet);
serverLogTextBox.AppendText($"\r\n[서버] -> [{targetClient.EndpointAddress}]: 화면 흔들기 요청 전송");
}
catch (Exception ex)
{
serverLogTextBox.AppendText($"\r\n화면 흔들기 전송 실패 ({targetClient.EndpointAddress}): {ex.Message}");
RemoveClient(targetClient.EndpointAddress);
}
}
}
// 메시지 패킷 생성 헬퍼
private byte[] CreateMessagePacket(string message, ChatMessageType type)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
byte[] packet = new byte[messageBytes.Length + 1];
packet[0] = (byte)type; // 첫 바이트는 메시지 타입
Buffer.BlockCopy(messageBytes, 0, packet, 1, messageBytes.Length);
return packet;
}
}
}
채팅 클라이언트 구현
채팅 클라이언트는 서버에 연결하고, 메시지를 서버로 전송하며, 서버로부터 수신된 메시지를 표시하는 역할을 합니다.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace ChatClientApp
{
// 메시지 유형 정의 (서버와 동일한 공통 모듈)
public enum ChatMessageType : byte
{
TextMessage = 1,
ScreenShake = 2
}
public partial class ClientMainForm : Form
{
private Socket _clientCommunicationSocket;
public ClientMainForm()
{
InitializeComponent();
}
private void ClientMainForm_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
serverIpTextBox.Text = "127.0.0.1"; // 기본 IP
serverPortTextBox.Text = "50000"; // 기본 포트
}
private void connectButton_Click(object sender, EventArgs e)
{
try
{
string ipAddr = serverIpTextBox.Text.Trim();
string portNum = serverPortTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(ipAddr) || string.IsNullOrWhiteSpace(portNum))
{
MessageBox.Show("서버 IP와 포트 번호를 입력하세요!");
return;
}
_clientCommunicationSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_clientCommunicationSocket.Connect(IPAddress.Parse(ipAddr), int.Parse(portNum));
connectionStatusLabel.Text = "서버에 연결되었습니다.";
// 메시지 수신을 위한 스레드 시작
Thread receiveThread = new Thread(ReceiveMessagesFromServer);
receiveThread.IsBackground = true;
receiveThread.Start();
}
catch (Exception ex)
{
MessageBox.Show($"서버 연결 실패: {ex.Message}");
connectionStatusLabel.Text = "서버 연결 실패.";
}
}
private void ReceiveMessagesFromServer()
{
byte[] receiveBuffer = new byte[4 * 1024 * 1024]; // 4MB 버퍼
while (true)
{
try
{
int bytesRead = _clientCommunicationSocket.Receive(receiveBuffer);
if (bytesRead <= 0)
{
// 서버 연결 종료
clientLogTextBox.AppendText("\r\n서버와의 연결이 끊어졌습니다.");
connectionStatusLabel.Text = "서버 연결 끊김.";
break;
}
ChatMessageType msgType = (ChatMessageType)receiveBuffer[0];
string messageData = Encoding.UTF8.GetString(receiveBuffer, 1, bytesRead - 1);
ProcessReceivedMessage(msgType, messageData);
}
catch (Exception)
{
clientLogTextBox.AppendText("\r\n메시지 수신 오류 또는 서버 연결 종료.");
connectionStatusLabel.Text = "수신 오류 및 연결 끊김.";
break;
}
}
_clientCommunicationSocket?.Close();
_clientCommunicationSocket?.Dispose();
}
private void ProcessReceivedMessage(ChatMessageType type, string message)
{
switch (type)
{
case ChatMessageType.TextMessage:
clientLogTextBox.AppendText($"\r\n[서버]: {message}");
break;
case ChatMessageType.ScreenShake:
// 화면 흔들기 효과 구현 (예시)
ShakeForm();
clientLogTextBox.AppendText($"\r\n[서버]: 화면 흔들기 요청!");
break;
default:
clientLogTextBox.AppendText($"\r\n[서버][알 수 없는 타입 {type}]: {message}");
break;
}
}
private void ShakeForm()
{
// 폼을 짧게 흔드는 시각적 효과
Point originalLocation = this.Location;
Random r = new Random();
for (int i = 0; i < 5; i++)
{
this.Location = new Point(originalLocation.X + r.Next(-10, 10), originalLocation.Y + r.Next(-10, 10));
Thread.Sleep(50);
}
this.Location = originalLocation;
}
private void sendMessageButton_Click(object sender, EventArgs e)
{
string messageContent = clientSendTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(messageContent))
{
MessageBox.Show("보낼 메시지를 입력하세요.");
return;
}
if (_clientCommunicationSocket == null || !_clientCommunicationSocket.Connected)
{
MessageBox.Show("서버에 연결되어 있지 않습니다.");
return;
}
try
{
byte[] packet = CreateMessagePacket(messageContent, ChatMessageType.TextMessage);
_clientCommunicationSocket.Send(packet);
clientLogTextBox.AppendText($"\r\n[나]: {messageContent}");
clientSendTextBox.Clear();
}
catch (Exception ex)
{
MessageBox.Show($"메시지 전송 실패: {ex.Message}");
clientLogTextBox.AppendText($"\r\n메시지 전송 실패: {ex.Message}");
}
}
// 메시지 패킷 생성 헬퍼 (서버와 동일)
private byte[] CreateMessagePacket(string message, ChatMessageType type)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
byte[] packet = new byte[messageBytes.Length + 1];
packet[0] = (byte)type; // 첫 바이트는 메시지 타입
Buffer.BlockCopy(messageBytes, 0, packet, 1, messageBytes.Length);
return packet;
}
}
}