2023.03.28 - [C# 프로그래밍] - [C#] TCP/IP 네트워크 (1)
[C#] TCP/IP 네트워크 (1)
네트워크 기술은 다른 컴퓨터에서 실행되고 있는 상대 프로세스가 마치 한 컴퓨터, 아니 한 프로세스 안에 있는 객체인 것처럼 다루게 해 줍니다. 이런 강력한 기술을 다루기는 기본적으로 어렵
onesside-world.tistory.com
2023.03.29 - [C# 프로그래밍] - [C#] TCP/IP 네트워크 (2)
[C#] TCP/IP 네트워크 (2)
[IP주소 체계 요약] IP 주소는 컴퓨터 네트워크에서 각 호스트를 식별하는 데 사용되는 고유한 식별자입니다. IPv4는 32비트 주소 체계이며, 총 4,294,967,296개의 주소를 가질 수 있습니다. IPv6는 128비
onesside-world.tistory.com
TcpClient와 TcpListener
TcpClient와 TcpListener는 TCP/IP 서버/클라이언트 동작 과정을 추상화환 .NET 프레임워크입니다.
TcpClient와 TcpListener는 모두 .NET에서 TCP 소켓 통신을 지원하는 클래스입니다. 또한, 둘 다 TCP 프로토콜을 사용하여 데이터를 송수신할 수 있습니다.
TcpListener 클래스
- TcpListener는 TCP 서버 역할을 수행하며, 클라이언트의 연결 요청을 수락하고 데이터를 송수신할 수 있습니다.
- TcpListener 클래스의 생성자는 포트 번호를 인자로 받아 서버를 시작합니다.
- TcpListener.AcceptTcpClient() 메서드를 사용하여 클라이언트의 연결 요청을 수락하고 TcpClient 객체를 반환합니다.
- 반환된 TcpClient 객체를 사용하여 클라이언트와 데이터를 송수신할 수 있습니다.
- TcpListener.Stop() 메서드를 사용하여 서버를 중지할 수 있습니다.
TcpClient 클래스
- 서버 애플리케이션과 클라이언트 애플리케이션 양쪽에서 모두 사용됩니다.
- TcpClient는 TCP 클라이언트 역할을 수행하며, 서버와 연결하여 데이터를 송수신할 수 있습니다.
- TcpClient 클래스의 생성자는 서버의 IP 주소와 포트 번호를 인자로 받아 연결을 시도합니다.
- 연결이 성공하면 NetworkStream 객체를 사용하여 데이터를 송수신할 수 있습니다.
- TcpClient.Close() 메서드를 사용하여 연결을 종료할 수 있습니다.
위의 그림은 서버와 클라이언트에서 TCP/IP 통신을 수행하기 위해 호출하는 TcpListener와 TcpClient, 그리고 NetworkStream 클래스의 메스드들의 흐름을 나타냅니다.
다음 표에 TcpListener와 TcpClient 클래스의 주요 메소드를 정리하였습니다.
TcpListener 클래스 메소드
시작 | Start() | TcpListener 개체를 수신 대기 상태로 만듭니다. |
중지 | Stop() | TcpListener 개체를 중지합니다. |
연결 수락 | AcceptTcpClient() | 클라이언트에서 연결 요청을 수락하고 연결된 TcpClient 개체를 반환합니다. |
연결 대기 | Pending() | 연결을 수락할 수 있는 클라이언트가 있는지 여부를 나타내는 값을 반환합니다. |
TcpClient 클래스 메소드
연결 | Connect(string, int) | 지정된 호스트와 포트 번호로 TCP 서버에 연결합니다. |
네트워크 스트림 | GetStream() | NetworkStream 개체를 반환하여 TcpClient와 연결된 네트워크 스트림을 가져옵니다. |
소켓 닫기 | Close() | TcpClient와 연결된 소켓을 닫습니다. |
메아리 서버 구현으로 서버 코드 분석하기
메아리 서버는 클라이언트에서 보낸 메시지를 서버에서 다시 클라이언트에게 보내는 코드입니다.
먼저 서버 코드를 보겠습니다.
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TCPServer
{
class Program
{
static readonly int SERVERPORT = 9000;
static readonly int BUFSIZE = 512;
static void Main(string[] args)
{
// 서버 소켓 생성 및 설정
Socket listenSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint serverEP = new IPEndPoint(IPAddress.Any, SERVERPORT);
listenSock.Bind(serverEP);
listenSock.Listen(5);
Console.WriteLine($"[TCP 서버] {SERVERPORT}번 포트에서 대기 중...");
while (true)
{
// 클라이언트 소켓 생성 및 스레드 생성
Socket clientSock = listenSock.Accept();
Console.WriteLine($"[TCP 서버] 클라이언트 접속: IP 주소={clientSock.RemoteEndPoint}");
Thread thread = new Thread(() => ProcessClient(clientSock));
thread.Start();
}
// 소켓 종료 및 리소스 반환
listenSock.Close();
}
static void ProcessClient(Socket clientSock)
{
try
{
// 클라이언트 소켓으로부터 데이터를 받아 처리하는 부분
while (true)
{
byte[] recvBytes = new byte[BUFSIZE];
int recvLen = clientSock.Receive(recvBytes);
if (recvLen == 0)
{
Console.WriteLine("[TCP 서버] 클라이언트 종료: IP 주소={0}", clientSock.RemoteEndPoint);
break;
}
string recvData = Encoding.UTF8.GetString(recvBytes, 0, recvLen);
Console.WriteLine("[TCP 서버] 받은 데이터: IP 주소={0}, 데이터={1}", clientSock.RemoteEndPoint, recvData);
byte[] sendData = Encoding.UTF8.GetBytes(recvData);
clientSock.Send(sendData);
}
}
catch (SocketException ex)
{
Console.WriteLine($"[TCP 서버] 소켓 예외 발생: {ex.Message}");
}
finally
{
clientSock.Close();
}
}
}
}
먼저 서버를 실행하고 대기시켜야 합니다.
그런 다음 클라이언트 코드를 실행해야 정상적인 메아리 서버가 작동합니다.
클라이언트 코드를 보겠습니다.
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace ClientApp
{
class Program
{
static void Main(string[] args)
{
try
{
// 서버 IP와 포트번호
string serverIp = "127.0.0.1";
int serverPort = 9000;
// 서버 연결
TcpClient client = new TcpClient();
client.Connect(serverIp, serverPort);
// 데이터 통신에 사용할 변수
byte[] sendBytes;
byte[] recvBytes = new byte[512];
int byteCount;
string data;
// 서버와 데이터 통신
while (true)
{
// 데이터 입력
Console.Write("\n[보낼 데이터] ");
data = Console.ReadLine();
// '\n' 문자 제거
data = data.TrimEnd('\n', '\r');
if (string.IsNullOrEmpty(data))
break;
// 데이터 보내기
NetworkStream stream = client.GetStream();
sendBytes = Encoding.UTF8.GetBytes(data);
stream.Write(sendBytes, 0, sendBytes.Length);
Console.WriteLine("[TCP 클라이언트] {0}바이트를 보냈습니다.", sendBytes.Length);
// 데이터 받기
byteCount = stream.Read(recvBytes, 0, recvBytes.Length);
if (byteCount == 0)
break;
// 받은 데이터 출력
data = Encoding.UTF8.GetString(recvBytes, 0, byteCount);
Console.WriteLine("[TCP 클라이언트] {0}바이트를 받았습니다.", byteCount);
Console.WriteLine("[받은 데이터] {0}", data);
}
// 연결 종료
client.Close();
}
catch (Exception ex)
{
Console.WriteLine("[TCP 클라이언트] 예외 발생: {0}", ex.Message);
}
}
}
}
▶127.0.0.1 주소
127.0.0.1은 루프백 주소(loopback address) 또는 로컬호스트(localhost)라고도 불리며, 자신의 컴퓨터를 가리키는 IP 주소입니다. 이 주소를 사용하면 컴퓨터 내부에서 네트워크 연결 없이도 자기 자신에게 요청을 보내거나 서비스를 제공할 수 있습니다. 예를 들어, 127.0.0.1을 서버 주소로 사용하면 클라이언트와 서버를 동일한 컴퓨터에서 실행하면서 TCP/IP 통신을 할 수 있습니다.
흐르는 패킷
TCP 프로토콜에서는 데이터를 한 번에 전송하지 않고 작은 단위로 분할하여 전송하며, 이렇게 분할된 작은 조각들을 패킷(Packet)이라고 부릅니다. 이 패킷들은 수신 측에서 다시 조합되어 최종적으로 전체 데이터를 완성합니다.
이때, TCP 송신 측에서는 데이터를 전송할 때마다 패킷을 만들어 네트워크를 통해 전송합니다. 하지만 수신 측에서는 패킷을 바로 처리하지 않고, 먼저 받은 패킷들을 일시적으로 저장해 둘 수 있는 버퍼를 가지고 있습니다. 이 버퍼는 마치 댐에서 물을 저장하는 것과 같은 역할을 합니다.
수신측에서는 버퍼에 저장된 패킷들을 하나씩 꺼내어 처리하며, 처리가 완료된 패킷은 버퍼에서 제거됩니다. 이러한 방식으로 TCP는 흐름제어(Flow Control)와 혼잡제어(Congestion Control)를 수행하며, 안정적이고 신뢰성 높은 데이터 전송을 보장합니다.
따라서 TCP 프로그래밍에서 Write() 함수를 호출할 때마다 전체 데이터가 한 번에 전송되는 것이 아니라, 데이터가 분할되어 작은 패킷으로 만들어져 전송되며, 이 패킷들은 수신측에서 버퍼를 통해 조합되어 최종적으로 전체 데이터를 완성하게 됩니다.
'C# 프로그래밍' 카테고리의 다른 글
[C#] LINQ (0) | 2023.04.14 |
---|---|
[C#] TCP/IP 네트워크 (2) (0) | 2023.03.29 |
[C#] TCP/IP 네트워크 (1) (1) | 2023.03.28 |
[C#] async 한정자와 await 연산자 (0) | 2023.03.28 |
[C#] task (2) (0) | 2023.03.27 |