C# ) C# 기초 (스레드 , 태스크 ,네트워크 프로그래밍)
스레드
** .NET은 스레드를 제어하는 클래스로 System.Threading.Thread를 제공합니다.
1. Thread의 인스턴스를 생성한다. 이때 생성자의 인수로 스레드가 실행할 메소드를 넘긴다.
2. Thread.Start() 메서드를 호출하여 스레드를 시작한다.
3. Thread.Join() 메서드를 호출하여 스레드가 끝날 때까지 기다린다.
static void DoSomething() // 스레드가 실행할 메서드
{
for(int i = 0; i < 5; i++)
{
Console.WriteLine("DoSomething : {0}", i);
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DoSomething));
// 1. Thread의 인스턴스 생성
t1.Start(); // 2. 스레드 시작
t1.Join(); // 3. 스레드 종료 대기
}
=> 실제 스레드가 메모리에 적재되는 시점은 t1.Start()메서드를 호출 했을 때
=> Thread 클래스의 인스턴스는 '준비' 만 해놓을 뿐 t1.Start() 메서드가 호출되고 나면, CLR은 스레드를 실제로 생성하여DoSomething() 메서드를 호출합니다.
=> t1.Join()메서드는 블록되어 있다가 DoSomething() 메서드의 실행이 끝나면, 다시 t1 스레드의 실행이 끝나면 반환되어 다음 코드를 실행 할 수 있게 합니다.
* CLR은 Common Language Runtime의 약자로, C# 프로그램이 실행되는 환경을 뜻함
* CLR 덕분에 C# 개발자들은 메모리 관리나 보안과 같은 복잡한 문제에 대해 걱정할 필요 없이 프로그램 개발에 집중할 수 있다.
using System;
using System.Threading;
namespace BasicThread
{
class MainApp
{
static void DoSomething()
{
for(int i = 0; i< 5; i++)
{
Console.WriteLine($"DoSomething : {i}");
Thread.Sleep(10);
// Sleep() 메서드를 만나면 인수(10)만큼
// CPU 사용을 멈춥니다. 이 때 인수 단위는 밀리초입니다.
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DoSomething));
Console.WriteLine("Starting thread...");
t1.Start();
for(int i = 0; i < 5; i++)
{
Console.WriteLine($"Main : {i}");
Thread.Sleep(10);
}
Console.WriteLine("Wating until thread stops...");
t1.Join();
Console.WriteLine("Finished");
}
}
}
스레드 상태변화
상태 | 설명 |
Unstarted | 스레드 객체를 생성한 후 Thread.Start() 메서드가 호출되기 전의 상태 |
Running | 스레드가 시작하여 동작 중인 상태 Unstarted 상태의 스레드를 Thread.Start()메서드를 통해 이상태로 만들 수 있다. |
Suspended | 스레드의 일시 중단 상태 스레드를 Thread.Suspend() 메서드를 통해 이 상태로 만들 수 있으며, Suspend() 상태인 스레드는 Thread.Resume()메서드를 통해 다시 Running 상태로 만들 수 있다. |
WaitSleepJoin | 스레드가 블록(Block)된 상태 스레드에 대해 Monitor.Enter(), Thread.Sleep(), Thread.Join() 메서드를 호출하면 이상태 |
Aborted | 스레드가 취소된 상태 Thread.Abort() 메서드를 호출하면 이 상태가 된다. Aborted 상태가 된 스레드는 다시 Stopped상태로 전환되어 완전히 중지됩니다. |
Stopped | 중지도니 스레드의 상태 Abort() 메서드를 호출하거나 스레드가 실행중인 메서드가 종료되면 이 상태가 된다. |
Background | 스레드가 백그라운드로 동작하고 있음을 나타낸다. 포어그라운드(Foreground)스레드는 하나라도 살아 있는 한 프로세스가 죽지 않지만, 백그라운드 하나가 아니라 열 개가 살아 있어도 프로세스가 죽고 사는 것에는 영향을 미치지 않습니다. 하지만 프로세스가 죽으면 백그라운드 스레들도 모두 죽습니다. Thread.IsBackground속성에 true 값을 입력함으로써 스레드를 이 상태로 바꿀 수 있다. |
** Aborted 상태의 스레드는 절대 Running상태로 전이되지 못하고, Running 상태의 스레드는 Unstarted 상태로 바꿀 수 없습니다.
using System;
using System.Threading;
namespace UsingThreadState
{
class MainApp
{
private static void PrintThreadState(ThreadState state)
{
Console.WriteLine("{0,-16} : {1}", state, (int)state);
}
static void Main(string[] args)
{
PrintThreadState(ThreadState.Running); // 실행 중인 상태
PrintThreadState(ThreadState.StopRequested); // 중지 요청된 상태
PrintThreadState(ThreadState.Background); // 백그라운드 상태
PrintThreadState(ThreadState.Unstarted); // 시작되지 않은 상태
PrintThreadState(ThreadState.Stopped); // 중지된 상태
PrintThreadState(ThreadState.WaitSleepJoin); // 대기, 절전, 또는 다른 스레드 조인 상태
PrintThreadState(ThreadState.Suspended); // 일시 중단된 상태
PrintThreadState(ThreadState.AbortRequested); // 중단 요청된 상태
PrintThreadState(ThreadState.Aborted); // 중단된 상태
PrintThreadState(ThreadState.Aborted | ThreadState.Stopped); // 중단 및 중지된 상태
}
}
}
=> 비트 연산을 통해 ThreadState가 어떤 상태에 있는지 쉽게 알아보기 위해 2제곱으로 증가된 값을 가짐
=> Stopped + Aborted (16 + 256) 더해져 272 상태로 출력됨
스레드 임의로 종료
Thread.Interrupt() 메서드는 스레드가 한참 동작 중인 상태(Running상태)를 피해서 WaitSleepJoin상태에 들어갔을 때
ThreadInterruptException 예외를 던져 스레드를 중지 시킵니다.
또한, WaitSleepJoin 상태에 있을 때는 즉시 중단 시키지만, 다른상태일 때는 스레드를 지켜보고 있다가 WaitSleepJoin상태가 괴면 그제서야 스레드를 중단시킵니다.
** 이런 특징 때문에 프로그래머는 최소한 코드가 '절대로 중단되면 안 되는' 작업을 하고 있을 때는 중단되지 않는다는 보장을 받을 수 있다.
static void DoSomethine()
{
try
{
for(int i = 0; i < 10000; i++)
{
Console.WriteLine("DoSomething : {0}", i);
Thread.Sleep(10);
}
}
catch(ThreadInterruptedException e)
{
//...
}
finally
{
//...
}
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DoSomethind));
t1.Start();
t1.Interrupt(); // 스레드 취소(종료)
t1.Join();
}
스레드 간 동기화
** 동기화 : 스레드들이 순서를 갖춰 자원을 사용하게 하는것을 뜻함
[ lock 키워드로 동기화 하기 ]
** 크리티컬 섹션 : 한번에 한 스레드만 사용할 수 있는 코드 영역
C#에서는 lock 키워드로 감싸주기만 해도 평범한 코드를 크리티컬 섹션으로 바꿀 수 있다.
class Counter
{
public int count = 0;
public void Increase()
{
count = count + 1;
}
// ...
CounterClass obj = new CounterClass();
Thread t1 = new Thread(new ThreadStart(obj.Increase));
Thread t2 = new Thread(new ThreadStart(obj.Increase));
Thread t3 = new Thread(new ThreadStart(obj.Increase));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();
Console.WriteLine(obj.count);
}
병렬처리와 비동기 처리의 차이
하나의 작업을 여러 작업자가 나눠서 수행한 뒤 다시 하나의 결과로 만드는것을 병렬 처리라 합니다. 이에 비해 비동기 처리는 작업A를 시작한 후 A의 결과가 나올 때까지 마냥 대기하는 대신 곧이어 다른 작업 B,C,D... 를 수행하다가 작업A가 끝나면 그 때 결과를 받아내는 처리 방식을 말한다.
[ 멀티 스레드 ]
System.Threading.Tasks 네임스페이스의 클래스들은 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 코드와 비동기 코드를 위해 설계됐습니다.
Task 클래스
Task 클래스는 인스턴스를 생성할 때 Action 대리자를 넘겨받습니다. 다시말해 반환형을 갖지 않은 메서드와 익명 메서드, 무명함수등을 넘겨받는다.
Action someAction = () =>
{
Thread.Sleep(1000);
Console.WriteLine("Printed asynchronously.");
};
Task myTask = new Task(someAction); //생성자에서 넘겨받은 무명 함수를 비동기로 호출합니다.
myTask.Start();
Console.WriteLine("Printed synchronously.");
myTask.Wait(); // myTask 비동기 호출이 완료될 때까지 기다립니다.
/* 결과는
Printed synchronously.
Printed asynchronously. */
=> Action대리자 기반의 무명 함수 선언
=> Task 생성자를 생성하여 Action대리자를 인수로 받아 Start()메서드를 호출하여 생성자에서 넘겨 받은 Actio 대리자를 비동기로 실행 시킨다.
=> 한편, 선언한 Action대리자는 Thread.Sleep(1000)코드 때문에 실행을 완료할 때까지 최소한 1초는 소요합니다. 그 전에 프로그램은 "Printed synchronously"를 출력하고 myTask.Wait() 메서드 호출부에 가서 myTask가 실행 중인 비동기 코드가 완료될 때까지 대기합니다.
=> 이렇게 대기하던 프로그램은 비동기로 실행하고 있던 매개변수에 전달된 someAction 대리자가 "Printed asynchronously"를 출력하고 나면 종료합니다.
** Task.Run() 메서드를 사용하여 단번에 처리한다.
using System; // 기본적인 기능을 사용하기 위한 네임스페이스
using System.IO; // 파일 입출력을 위한 네임스페이스
using System.Threading; // 스레드 관련 기능을 위한 네임스페이스
using System.Threading.Tasks; // Task를 사용하기 위한 네임스페이스
using System.Collections.Generic; // List<T>와 같은 컬렉션을 사용하기 위한 네임스페이스
namespace UsingTask // 네임스페이스 선언
{
class MainApp // 클래스 선언
{
static void Main(string[] args) // 프로그램 시작 지점
{
string srcFile = args[0]; // 프로그램 실행 시 입력받은 첫 번째 인자를 파일 경로로 사용
Action<object> FileCopyAction = (object state) => // 파일 복사 작업을 수행하는 델리게이트
{
String[] paths = (String[])state; // 델리게이트에 전달된 상태 정보를 문자열 배열로 변환
File.Copy(paths[0], paths[1]); // 파일 복사
Console.WriteLine("TaskID:{0}, ThreadID:{1}, {2} was copied to {3}",
Task.CurrentId, Thread.CurrentThread.ManagedThreadId,
paths[0], paths[1]); // Task ID, 스레드 ID, 원본 파일 경로, 복사된 파일 경로 출력
};
Task t1 = new Task( // 첫 번째 파일 복사 Task 생성
FileCopyAction, // 수행할 작업
new string[] { srcFile, srcFile + ".copy1" }); // 작업에 필요한 상태 정보 (원본 파일 경로, 복사될 파일 경로)
Task t2 = Task.Run(() => // 두 번째 파일 복사 Task 생성 (Task.Run을 사용)
{
FileCopyAction(new string[] { srcFile, srcFile + ".copy2" }); // 델리게이트를 직접 호출
});
t1.Start(); // 첫 번째 Task 시작
Task t3 = new Task( // 세 번째 파일 복사 Task 생성
FileCopyAction,
new string[] { srcFile, srcFile + ".copy3" });
t3.RunSynchronously(); // 세 번째 Task를 동기적으로 실행
t1.Wait(); // 첫 번째 Task가 완료될 때까지 대기
t2.Wait(); // 두 번째 Task가 완료될 때까지 대기
t3.Wait(); // 세 번째 Task가 완료될 때까지 대기
var myTask = Task<List<int>>.Run( // 결과 값을 반환하는 Task 생성 (Task<T> 사용)
() =>
{
Thread.Sleep(1000); // 1초 동안 대기
List<int> list = new List<int>(); // 정수 리스트 생성
list.Add(3); // 리스트에 3 추가
list.Add(4); // 리스트에 4 추가
list.Add(5); // 리스트에 5 추가
return list; // 리스트 반환
}
);
myTask.Wait(); // Task가 완료될 때까지 대기
}
}
}
=> Task를 이용해서 세 개의 파일을 복사합니다. 앞의 두 Task는 비동기로 파일을 복사하고, 세 번째 Task는 동기로 파일을 복사합니다.
=> FileCopyAction 델리게이트는 파일 복사 작업을 수행하고, Task ID, 스레드 ID, 원본 파일 경로, 복사된 파일 경로를 출력합니다.
=> t1, t2, t3는 각각 파일 복사 작업을 수행하는 Task입니다.
=> t1은 Task 클래스의 생성자를 사용하여 생성하고 Start() 메서드로 시작합니다.
=> t2는 Task.Run() 메서드를 사용하여 생성하고 시작합니다.
=> t3는 RunSynchronously() 메서드를 사용하여 동기적으로 실행합니다.
=> Wait() 메서드는 Task가 완료될 때까지 기다립니다.
=> myTask는 Task<List<int>>.Run() 메서드를 사용하여 결과 값으로 정수 리스트를 반환하는 Task입니다.
Task를 사용하는 방법:
- Task 생성: Task 클래스의 생성자를 사용하여 Task를 생성합니다.
- 작업 시작: Start() 메서드를 호출하여 Task를 시작합니다. 또는 Task.Run() 메서드를 사용하여 Task를 생성하고 바로 시작할 수도 있습니다.
- 결과 값 가져오기: Result 속성을 사용하여 Task의 결과 값을 가져옵니다. (Task<T>를 사용하는 경우)
- 상태 확인: Status 속성을 사용하여 Task의 상태를 확인합니다.
- 대기: Wait() 메서드를 사용하여 Task가 완료될 때까지 기다립니다.
- 취소: CancellationTokenSource 클래스를 사용하여 Task를 취소합니다.
- 연속 작업: ContinueWith() 메서드를 사용하여 Task를 연결합니다. Task.WhenAll() 메서드를 사용하여 여러 Task가 완료될 때까지 기다릴 수 있습니다.
using System;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
// Task 생성 및 실행
Task task = Task.Run(() =>
{
Console.WriteLine("Hello from task!");
});
// Task 완료 대기
task.Wait();
}
}
=> Task.Run() 메서드를 사용하여 간단한 Task를 생성하고 실행합니다. Wait() 메서드는 Task가 완료될 때까지 기다립니다.
Task 사용하는 예시
1. 웹 페이지 다운로드
웹 페이지를 다운로드하는 작업은 시간이 오래 걸릴 수 있기 때문에, Task를 사용하여 비동기적으로 처리하면 사용자 인터페이스가 멈추는 것을 방지할 수 있습니다.
using System;
using System.Net;
using System.Threading.Tasks;
class Example
{
static async Task Main(string[] args)
{
// 웹 페이지 주소
string url = "https://www.example.com";
// Task를 사용하여 웹 페이지 다운로드
using (WebClient client = new WebClient())
{
Task<string> downloadTask = client.DownloadStringTaskAsync(url);
// 다른 작업 수행
Console.WriteLine("다른 작업을 수행 중입니다...");
// 웹 페이지 다운로드 완료 대기
string result = await downloadTask;
// 다운로드된 웹 페이지 내용 출력
Console.WriteLine(result);
}
}
}
2. 이미지 처리
여러 이미지를 처리하는 작업은 각 이미지 처리를 Task로 만들어 병렬로 실행하면 속도를 높일 수 있습니다.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
// 이미지 파일 경로 리스트
List<string> imageFiles = new List<string>() { "image1.jpg", "image2.png", "image3.bmp" };
// 각 이미지 파일을 처리하는 Task 생성
List<Task> tasks = new List<Task>();
foreach (string imageFile in imageFiles)
{
tasks.Add(Task.Run(() =>
{
// 이미지 로드
Bitmap image = new Bitmap(imageFile);
// 이미지 처리 작업 수행
// ...
// 처리된 이미지 저장
// ...
}));
}
// 모든 Task가 완료될 때까지 대기
Task.WaitAll(tasks.ToArray());
Console.WriteLine("모든 이미지 처리가 완료되었습니다.");
}
}
3. 파일 압축
큰 파일을 압축하는 작업은 시간이 오래 걸릴 수 있으므로 Task를 사용하여 비동기적으로 처리하면 좋습니다.
using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
// 압축할 파일 경로
string sourceFile = "data.txt";
// 압축 파일 경로
string compressedFile = "data.zip";
// Task를 사용하여 파일 압축
Task compressTask = Task.Run(() =>
{
using (FileStream sourceStream = new FileStream(sourceFile, FileMode.Open))
{
using (FileStream compressedStream = new FileStream(compressedFile, FileMode.Create))
{
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
sourceStream.CopyTo(gzipStream);
}
}
}
});
// 압축 작업 완료 대기
compressTask.Wait();
Console.WriteLine("파일 압축이 완료되었습니다.");
}
}
코드의 비동기 실행 결과를 주는 Task <TResult> 클래스
일반적인 Task는 작업을 수행하지만 결과 값을 반환하지 않는 반면, Task<TResult>는 작업을 수행하고 특정 타입의 결과 값을 반환합니다.
** Task<TResult>: 비동기 작업을 실행하고 그 결과 값을 받아야 할 때 사용. 예를 들어, 웹 서버에서 데이터를 가져오거나 복잡한 계산을 수행한 후 결과를 받아야 하는 경우에 유용
Task<TResult>를 사용하는 방법:
- Task 생성: Task<TResult> 클래스의 생성자를 사용하여 Task를 생성합니다. 이때 생성자에 결과 값을 반환하는 델리게이트를 전달해야 합니다.
- 작업 시작: Start() 메서드를 호출하여 Task를 시작합니다. 또는 Task.Run() 메서드를 사용하여 Task를 생성하고 바로 시작할 수도 있습니다.
- 결과 값 가져오기: Result 속성을 사용하여 Task의 결과 값을 가져옵니다. Result 속성은 Task가 완료될 때까지 기다렸다가 결과 값을 반환합니다.
- 예외 처리: 비동기 작업에서 예외가 발생하면 Result 속성을 통해 예외가 throw됩니다. try-catch 블록을 사용하여 예외를 처리할 수 있습니다.
using System;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
// 1부터 10까지의 합을 계산하는 Task 생성
Task<int> sumTask = Task.Run(() =>
{
int sum = 0;
for (int i = 1; i <= 10; i++)
{
sum += i;
}
return sum;
});
// Task 완료 대기 및 결과 값 출력
int result = sumTask.Result;
Console.WriteLine($"1부터 10까지의 합: {result}"); // 출력: 1부터 10까지의 합: 55
}
}
=> Task.Run() 메서드를 사용하여 1부터 10까지의 합을 계산하는 Task를 생성하고, Result 속성을 사용하여 결과 값을 가져옵니다.
병렬처리 Parallel 클래스
System.Threading. Tasks.Parallel 클래스
** 여러 개의 CPU 코어를 활용하여 작업을 동시에 처리하고 프로그램의 성능을 향상시키는 데 사용됩니다.
Parallel 클래스의 주요 기능:
- Parallel.For(): for 루프를 병렬로 실행합니다. 배열이나 리스트의 각 요소에 대해 반복 작업을 수행할 때 유용합니다.
- Parallel.ForEach(): foreach 루프를 병렬로 실행합니다. 컬렉션의 각 요소에 대해 반복 작업을 수행할 때 유용합니다.
- Parallel.Invoke(): 여러 개의 작업을 병렬로 실행합니다. 서로 독립적인 작업을 동시에 실행할 때 유용합니다.
using System;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
Parallel.For(1, 11, i =>
{
Console.WriteLine($"{i}의 제곱 = {i * i}");
});
}
}
=> Parallel.For(): 1부터 10까지의 제곱을 계산
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
List<string> words = new List<string>() { "apple", "banana", "cherry" };
Parallel.ForEach(words, word =>
{
Console.WriteLine(word.ToUpper());
});
}
}
=> Parallel.ForEach(): 문자열 리스트의 각 요소를 대문자로 변환\
using System;
using System.Threading.Tasks;
class Example
{
static void Main(string[] args)
{
Parallel.Invoke(
() => Console.WriteLine("작업 1 실행"),
() => Console.WriteLine("작업 2 실행"),
() => Console.WriteLine("작업 3 실행")
);
}
}
=> Parallel.Invoke(): 세 개의 작업을 병렬로 실행
async 한정자와 await 연산자로 만드는 비동기
"async로 한정한 Task 또는 Task<TResult>를 반환하는 메소드/태스크/람다식은 await 연산자를 만나는 곳에서 호출자에게 제어를 돌려주며, await 연산자가 없는 경우 동기로 실행됩니다.”
- async: 메서드에 async 키워드를 붙이면 해당 메서드가 비동기 메서드가 됩니다. 비동기 메서드는 Task 또는 Task<TResult> 객체를 반환하며, 백그라운드에서 작업을 수행합니다.
- await: await 키워드는 비동기 작업이 완료될 때까지 기다렸다가 결과를 반환합니다. await 키워드를 사용하면 마치 동기 메서드처럼 코드를 작성할 수 있어서 비동기 프로그래밍이 훨씬 쉬워집니다.
** TaskDelay() 함수가 하는 일은 인수로 입력받은 시간이 지나먼 Task 객체를 반환하는 것입니다.
즉, Task.Delay()는 Thread.Sleep()의 비동기 버전이라고 할 수 있다.
async와 await의 장점:
- 코드 간결성: 비동기 코드를 동기 코드처럼 작성할 수 있어서 코드가 간결해집니다.
- 가독성: await 키워드를 사용하여 비동기 작업의 흐름을 명확하게 표현할 수 있습니다.
- 유지보수: 코드가 간결하고 가독성이 높아서 유지보수가 용이합니다.
- 성능: 비동기 작업을 효율적으로 처리하여 프로그램의 성능을 향상시킬 수 있습니다.
사용예시
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Example
{
static async Task Main(string[] args)
{
// 웹 페이지 내용을 가져오는 비동기 메서드
async Task<string> DownloadWebPageAsync(string url)
{
using (HttpClient client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
// 웹 페이지 주소
string url = "https://www.example.com";
// 웹 페이지 내용을 비동기적으로 가져옴
string result = await DownloadWebPageAsync(url);
// 가져온 내용 출력
Console.WriteLine(result);
}
}
네트워크 프로그래밍
TcpListener와 TcpClient
[ TcpListener ]
- 서버역할
- 연결 수락 : 클라이언트의 연결 요청이 오면 AcceptTcpClient() 메서드를 사용하여 연결 수락하고 TcpClient 객체를 반환
[ TcpClient ]
- 클라이언트 역할
- 연결요청 : Connect() 메서드를 사용하여 서버에 연결요청
- 데이터 송수신 : GetStream() 메서드를 사용하여 NetworkStream 객체를 얻고, 이를통해 서버와 데이터를 주고받을 수 있습니다.
[ TcpListener와 TcpClient는 네트워크 프로그래밍의 기본적인 구성 요소 ]
// 서버
using System.Net;
using System.Net.Sockets;
TcpListener server = new TcpListener(IPAddress.Any, 8080);
server.Start();
TcpClient client = server.AcceptTcpClient();
NetworkStream stream = client.GetStream();
// 클라이언트
using System.Net.Sockets;
TcpClient client = new TcpClient("127.0.0.1", 8080);
NetworkStream stream = client.GetStream();
주요 메서드
클래스 | 메서드 | 설명 |
TcpListener | Start() | 연결 요청 수신 대기를 시작합니다. |
AcceptTcpClient() | 클라이언트의 연결 요청을 수락합니다. 이 메소드는 TcpClient 객체를 반환합니다. |
|
Stop() | 연결요청 수신 대기를 종료합니다. | |
TcpClient | Connect() | 서버에 연결을 요청합니다. |
GetStream() | 데이터를 주고받는데 사용하는 매개체인 NetworkStream을 가져옵니다. | |
Close() | 연결을 닫습니다. |
[ Server ]
using System;
using System.Diagnostics; // 프로세스 정보에 접근하기 위한 네임스페이스
using System.Net; // 네트워크 관련 클래스를 사용하기 위한 네임스페이스
using System.Net.Sockets; // 소켓 통신을 위한 네임스페이스
using System.Text; // 문자열 인코딩을 위한 네임스페이스
namespace EchoServer
{
class MainApp
{
static void Main(string[] args)
{
if (args.Length < 1) // 명령줄 인자가 없는 경우
{
Console.WriteLine("사용법 : {0} <Bind IP>",
Process.GetCurrentProcess().ProcessName); // 프로그램 사용법 출력
return; // 프로그램 종료
}
string bindIp = args[0]; // 첫 번째 명령줄 인자를 IP 주소로 사용
const int bindPort = 5425; // 포트 번호 5425로 설정
TcpListener server = null; // TcpListener 객체 초기화
try // 예외 처리 시작
{
IPEndPoint localAddress =
new IPEndPoint(IPAddress.Parse(bindIp), bindPort); // IP 주소와 포트 번호를 사용하여 IPEndPoint 객체 생성
server = new TcpListener(localAddress); // TcpListener 객체 생성
server.Start(); // 서버 시작
Console.WriteLine("메아리 서버 시작... "); // 서버 시작 메시지 출력
while (true) // 무한 루프
{
TcpClient client = server.AcceptTcpClient(); // 클라이언트 연결 수락
Console.WriteLine("클라이언트 접속 : {0} ",
((IPEndPoint)client.Client.RemoteEndPoint).ToString()); // 클라이언트 접속 정보 출력
NetworkStream stream = client.GetStream(); // 네트워크 스트림 생성
int length; // 데이터 길이 저장 변수
string data = null; // 데이터 저장 변수
byte[] bytes = new byte[256]; // 데이터 수신 버퍼
while ((length = stream.Read(bytes, 0, bytes.Length)) != 0) // 데이터 수신
{
data = Encoding.Default.GetString(bytes, 0, length); // 바이트 배열을 문자열로 변환
Console.WriteLine(String.Format("수신: {0}", data)); // 수신 데이터 출력
byte[] msg = Encoding.Default.GetBytes(data); // 문자열을 바이트 배열로 변환
stream.Write(msg, 0, msg.Length); // 데이터 전송
Console.WriteLine(String.Format("송신: {0}", data)); // 송신 데이터 출력
}
stream.Close(); // 스트림 닫기
client.Close(); // 클라이언트 연결 닫기
}
}
catch (SocketException e) // 소켓 예외 처리
{
Console.WriteLine(e); // 예외 정보 출력
}
finally // 예외 발생 여부와 관계없이 실행
{
server.Stop(); // 서버 중지
}
Console.WriteLine("서버를 종료합니다.");
}
}
}
=> TcpListener 클래스를 사용하여 서버 소켓을 생성하고, TcpClient 클래스를 사용하여 클라이언트 소켓을 생성합니다.
=> NetworkStream 클래스를 사용하여 클라이언트와 데이터를 주고받습니다.
[ Client ]
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace EchoClient
{
class MainApp
{
static void Main(string[] args)
{
if (args.Length < 4) // 명령줄 인자가 4개 미만인 경우
{
Console.WriteLine( // 프로그램 사용법 출력
"사용법 : {0} <Bind IP> <Bind Port> <Server IP> <Message>",
Process.GetCurrentProcess().ProcessName);
return; // 프로그램 종료
}
string bindIp = args[0]; // 첫 번째 명령줄 인자를 클라이언트 IP 주소로 사용
int bindPort = Convert.ToInt32(args[1]); // 두 번째 명령줄 인자를 클라이언트 포트 번호로 사용
string serverIp = args[2]; // 세 번째 명령줄 인자를 서버 IP 주소로 사용
const int serverPort = 5425; // 서버 포트 번호 5425로 설정
string message = args[3]; // 네 번째 명령줄 인자를 메시지로 사용
try // 예외 처리 시작
{
IPEndPoint clientAddress =
new IPEndPoint(IPAddress.Parse(bindIp), bindPort); // 클라이언트 IP 주소와 포트 번호를 사용하여 IPEndPoint 객체 생성
IPEndPoint serverAddress =
new IPEndPoint(IPAddress.Parse(serverIp), serverPort); // 서버 IP 주소와 포트 번호를 사용하여 IPEndPoint 객체 생성
Console.WriteLine("클라이언트: {0}, 서버:{1}",
clientAddress.ToString(), serverAddress.ToString()); // 클라이언트와 서버 정보 출력
TcpClient client = new TcpClient(clientAddress); // TcpClient 객체 생성
client.Connect(serverAddress); // 서버에 연결
byte[] data = System.Text.Encoding.Default.GetBytes(message); // 메시지를 바이트 배열로 변환
NetworkStream stream = client.GetStream(); // 네트워크 스트림 생성
stream.Write(data, 0, data.Length); // 데이터 전송
Console.WriteLine("송신: {0}", message); // 송신 메시지 출력
data = new byte[256]; // 데이터 수신 버퍼
string responseData = ""; // 수신 데이터 저장 변수
int bytes = stream.Read(data, 0, data.Length); // 데이터 수신
responseData = Encoding.Default.GetString(data, 0, bytes); // 바이트 배열을 문자열로 변환
Console.WriteLine("수신: {0}", responseData); // 수신 메시지 출력
stream.Close(); // 스트림 닫기
client.Close(); // 클라이언트 연결 닫기
}
catch (SocketException e) // 소켓 예외 처리
{
Console.WriteLine(e); // 예외 정보 출력
}
Console.WriteLine("클라이언트를 종료합니다."); // 클라이언트 종료 메시지 출력
}
}
}
파일 업로드 서버와 클라이언트 구현
1. 서버/ 클라이언트 공용 클래스 라이브러리 구현
2. 서버 구현
3. 클라이언트 구현
1. 서버 / 클라이언트 공용 클래스 라이브러리
[ Message.cs ]
using System.Reflection.PortableExecutable;
namespace FUP
{
public class CONSTANTS
{
// 메시지 타입 (MSGTYPE) 상수 정의
public const uint REQ_FILE_SEND = 0x01;
public const uint REP_FILE_SEND = 0x02;
public const uint FILE_SEND_DATA = 0x03;
public const uint FILE_SEND_RES = 0x04;
public const byte NOT_FRAGMENTED = 0x00;
public const byte FRAGMENTED = 0x01;
public const byte NOT_LASTMSG = 0x00;
public const byte LASTMSG = 0x01;
public const byte ACCEPTED = 0x00;
public const byte DENIED = 0x01;
public const byte FAIL = 0x00;
public const byte SUCCESS = 0x01;
}
public interface ISerializable // 메시지, 헤더, 바디는 모두 이 인터페이스를 상속합니다.
// 즉, 이들은 자신의 데이터를 바이트 배열로 변환하고 그 바이트 배열의 크기를 반환해야합니다.
{
byte[] GetBytes();
int GetSize();
}
public class Message : ISerializable // FUP의 메시지를 나타내는 클래스, Header와 Body로 구성됩니다.
{
public Header Header { get; set; }
public ISerializable Body { get; set; }
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
Header.GetBytes().CopyTo(bytes, 0);
Body.GetBytes().CopyTo(bytes, Header.GetSize());
return bytes;
}
public int GetSize()
{
return Header.GetSize() + Body.GetSize();
}
}
}
[ Header.cs ]
using System;
namespace FUP
{
public class Header : ISerializable
{
public uint MSGID { get; set; }
public uint MSGTYPE { get; set; }
public uint BODYLEN { get; set; }
public byte FRAGMENTED { get; set; }
public byte LASTMSG { get; set; }
public ushort SEQ { get; set; }
public Header() { }
public Header(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
MSGTYPE = BitConverter.ToUInt32(bytes, 4);
BODYLEN = BitConverter.ToUInt32(bytes, 8);
FRAGMENTED = bytes[12];
LASTMSG = bytes[13];
SEQ = BitConverter.ToUInt16(bytes, 14);
}
public byte[] GetBytes()
{
byte[] bytes = new byte[16];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
temp = BitConverter.GetBytes(MSGTYPE);
Array.Copy(temp, 0, bytes, 4, temp.Length);
temp = BitConverter.GetBytes(BODYLEN);
Array.Copy(temp, 0, bytes, 8, temp.Length);
bytes[12] = FRAGMENTED;
bytes[13] = LASTMSG;
temp = BitConverter.GetBytes(SEQ);
Array.Copy(temp, 0, bytes, 14, temp.Length);
return bytes;
}
public int GetSize()
{
return 16;
}
}
}
[ Body. cs ]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FUP
{
public class BodyRequest : ISerializable // 파일 전송 요청 메시지에 사용할 본문 클래스
// FileSize와 FileName 필드를 가집니다.
{
public long FILESIZE;
public byte[] FILENAME;
public BodyRequest() { }
public BodyRequest(byte[] bytes)
{
FILESIZE = BitConverter.ToInt64(bytes, 0);
FILENAME = new byte[bytes.Length - sizeof(long)];
Array.Copy(bytes, sizeof(long), FILENAME, 0, FILENAME.Length);
}
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(FILESIZE);
Array.Copy(temp, 0, bytes, 0, temp.Length);
Array.Copy(FILENAME, 0, bytes, temp.Length, FILENAME.Length);
return bytes;
}
public int GetSize()
{
return sizeof(long) + FILENAME.Length;
}
}
public class BodyResponse : ISerializable // 파일 전송 요청에 대한 응답에 사용할 본문클래스
// MsgID와 Response 필드를 가집니다.
{
public uint MSGID;
public byte RESPONSE;
public BodyResponse() { }
public BodyResponse(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
RESPONSE = bytes[4];
}
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
bytes[temp.Length] = RESPONSE;
return bytes;
}
public int GetSize()
{
return sizeof(uint) + sizeof(byte);
}
}
public class BodyData : ISerializable // 실제 파일을 전송하는 메시지에 사용할 본문클래스
// 앞서 프로토콜 정의에서 이야기했던 것처럼 DATA필드만 갖고있습니다.
{
public byte[] DATA;
public BodyData(byte[] bytes)
{
DATA = new byte[bytes.Length];
bytes.CopyTo(DATA, 0);
}
public byte[] GetBytes()
{
return DATA;
}
public int GetSize()
{
return DATA.Length;
}
}
public class BodyResult : ISerializable // 파일 전송 결과 메시지에 사용할 본문 클래스
// 요청 메시지의 MsgID와 성공여부를 나타내는 Result 프로퍼티를 가집니다.
{
public uint MSGID;
public byte RESULT;
public BodyResult() { }
public BodyResult(byte[] bytes)
{
MSGID = BitConverter.ToUInt32(bytes, 0);
RESULT = bytes[4];
}
public byte[] GetBytes()
{
byte[] bytes = new byte[GetSize()];
byte[] temp = BitConverter.GetBytes(MSGID);
Array.Copy(temp, 0, bytes, 0, temp.Length);
bytes[temp.Length] = RESULT;
return bytes;
}
public int GetSize()
{
return sizeof(uint) + sizeof(byte);
}
}
}
[ MessageUtill.cs ]
using System;
using System.IO;
namespace FUP
{
public class MessageUtil
{
public static void Send(Stream writer, Message msg) // Send() 메서드는 스트림을 통해 메시지를 내보냅니다.
{
writer.Write(msg.GetBytes(), 0, msg.GetSize());
}
public static Message Receive(Stream reader)
{
int totalRecv = 0;
int sizeToRead = 16;
byte[] hBuffer = new byte[sizeToRead];
while (sizeToRead > 0)
{
byte[] buffer = new byte[sizeToRead];
int recv = reader.Read(buffer, 0, sizeToRead);
if (recv == 0)
return null;
buffer.CopyTo(hBuffer, totalRecv);
totalRecv += recv;
sizeToRead -= recv;
}
Header header = new Header(hBuffer);
totalRecv = 0;
byte[] bBuffer = new byte[header.BODYLEN];
sizeToRead = (int)header.BODYLEN;
while (sizeToRead > 0)
{
byte[] buffer = new byte[sizeToRead];
int recv = reader.Read(buffer, 0, sizeToRead);
if (recv == 0)
return null;
buffer.CopyTo(bBuffer, totalRecv);
totalRecv += recv;
sizeToRead -= recv;
}
ISerializable body = null;
switch (header.MSGTYPE) // 헤더의 MSGTYPE 프로퍼티를 통해 어떤 Body 클래스의 생성자를 호출할지 결정합니다.
{
case CONSTANTS.REQ_FILE_SEND:
body = new BodyRequest(bBuffer);
break;
case CONSTANTS.REP_FILE_SEND:
body = new BodyResponse(bBuffer);
break;
case CONSTANTS.FILE_SEND_DATA:
body = new BodyData(bBuffer);
break;
case CONSTANTS.FILE_SEND_RES:
body = new BodyResult(bBuffer);
break;
default:
throw new Exception(
String.Format(
"Unknown MSGTYPE : {0}" + header.MSGTYPE));
}
return new Message() { Header = header, Body = body };
}
}
}
2. 서버 구현
[ MainApp.cs ]
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using FUP;
namespace FileReceiver
{
class MainApp
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("사용법 : {0} <Directory>",
Process.GetCurrentProcess().ProcessName);
return;
}
uint msgId = 0;
string dir = args[0];
if (Directory.Exists(dir) == false)
Directory.CreateDirectory(dir);
const int bindPort = 5425; // 서버 포트 설정
TcpListener server = null;
try
{
IPEndPoint localAddress =
new IPEndPoint(0, bindPort); // IP주소를 0으로 입력하면 127.0.0.1뿐 아니라 OS에 할당되어 있는
// 어떤 주소로도 서버에 접속할 수 있습니다.
server = new TcpListener(localAddress);
server.Start();
Console.WriteLine("파일 업로드 서버 시작... ");
while (true)
{
TcpClient client = server.AcceptTcpClient();
Console.WriteLine("클라이언트 접속 : {0} ",
((IPEndPoint)client.Client.RemoteEndPoint).ToString());
NetworkStream stream = client.GetStream();
Message reqMsg = MessageUtil.Receive(stream); // 클라이언트가 보내온 파일 전송 요청 메시지 수신
if (reqMsg.Header.MSGTYPE != CONSTANTS.REQ_FILE_SEND)
{
stream.Close();
client.Close();
continue;
}
BodyRequest reqBody = (BodyRequest)reqMsg.Body;
Console.WriteLine(
"파일 업로드 요청이 왔습니다. 수락하시겠습니까? yes/no");
string answer = Console.ReadLine();
Message rspMsg = new Message();
rspMsg.Body = new BodyResponse()
{
MSGID = reqMsg.Header.MSGID,
RESPONSE = CONSTANTS.ACCEPTED
};
rspMsg.Header = new Header()
{
MSGID = msgId++,
MSGTYPE = CONSTANTS.REP_FILE_SEND,
BODYLEN = (uint)rspMsg.Body.GetSize(),
FRAGMENTED = CONSTANTS.NOT_FRAGMENTED,
LASTMSG = CONSTANTS.LASTMSG,
SEQ = 0
};
if (answer != "yes")
{
rspMsg.Body = new BodyResponse() // 사용자가 'yes'가 아닌 답을 입력하면 클라이언트에게 '거부'응답을 보냄
{
MSGID = reqMsg.Header.MSGID,
RESPONSE = CONSTANTS.DENIED
};
MessageUtil.Send(stream, rspMsg); // 물론 'yes'를 입력하면 클라이언트에게 '승낙'응답을 보냄
stream.Close();
client.Close();
continue;
}
else
MessageUtil.Send(stream, rspMsg);
Console.WriteLine("파일 전송을 시작합니다...");
long fileSize = reqBody.FILESIZE;
string fileName = Encoding.Default.GetString(reqBody.FILENAME);
FileStream file =
new FileStream(dir + "\\" + fileName, FileMode.Create); // FileMode : 업로드 파일 스트림을 생성
uint? dataMsgId = null;
ushort prevSeq = 0;
while ((reqMsg = MessageUtil.Receive(stream)) != null)
{
Console.Write("#");
if (reqMsg.Header.MSGTYPE != CONSTANTS.FILE_SEND_DATA)
break;
if (dataMsgId == null)
dataMsgId = reqMsg.Header.MSGID;
else
{
if (dataMsgId != reqMsg.Header.MSGID)
break;
}
if (prevSeq++ != reqMsg.Header.SEQ)
{
Console.WriteLine("{0}, {1}", prevSeq, reqMsg.Header.SEQ); // 메시지 순서가 어긋나면 전송을 중단
break;
}
file.Write(reqMsg.Body.GetBytes(), 0, reqMsg.Body.GetSize());
// Write : 전송받은 스트림을 서버에서 생성한 파일에 기록
if (reqMsg.Header.LASTMSG == CONSTANTS.LASTMSG)
break;
}
long recvFileSize = file.Length;
file.Close();
Console.WriteLine();
Console.WriteLine("수신 파일 크기 : {0} bytes", recvFileSize);
Message rstMsg = new Message();
rstMsg.Body = new BodyResult()
{
MSGID = reqMsg.Header.MSGID,
RESULT = CONSTANTS.SUCCESS
};
rstMsg.Header = new Header()
{
MSGID = msgId++,
MSGTYPE = CONSTANTS.FILE_SEND_RES,
BODYLEN = (uint)rstMsg.Body.GetSize(),
FRAGMENTED = CONSTANTS.NOT_FRAGMENTED, // NOT_FRAGMENTED : 분할 메시지가 아니라면 반복을 한번만 하고 빠져나옴
LASTMSG = CONSTANTS.LASTMSG, // LASTMSG: 마지막 메시지면 반복문을 빠져나옴
SEQ = 0
};
if (fileSize == recvFileSize)
MessageUtil.Send(stream, rstMsg);// 파일 전송 요청에 담겨온 파일 크기와 실제로 받은 파일크기를 비교하여 같으면 성공메시지 보냄
else
{
rstMsg.Body = new BodyResult()
{
MSGID = reqMsg.Header.MSGID,
RESULT = CONSTANTS.FAIL
};
MessageUtil.Send(stream, rstMsg); // 파일 크기에 이상이 있다면 실패 메시지를 보냄
}
Console.WriteLine("파일 전송을 마쳤습니다.");
stream.Close();
client.Close();
}
}
catch (SocketException e)
{
Console.WriteLine(e);
}
finally
{
server.Stop();
}
Console.WriteLine("서버를 종료합니다.");
}
}
}
3. 클라이언트 구현
[ MainApp.cs ]
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection.PortableExecutable;
using FUP;
using Microsoft.VisualBasic;
namespace FileSender
{
class MainApp
{
const int CHUNK_SIZE = 4096;
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine(
"사용법 : {0} <Server IP> <File Path>",
Process.GetCurrentProcess().ProcessName);
return;
}
string serverIp = args[0];
const int serverPort = 5425;
string filepath = args[1];
try
{
IPEndPoint clientAddress = new IPEndPoint(0, 0); // 클라이언트는 OS에서 할당한 IP주소와 포트에 바인딩
IPEndPoint serverAddress =
new IPEndPoint(IPAddress.Parse(serverIp), serverPort);
Console.WriteLine("클라이언트: {0}, 서버:{1}",
clientAddress.ToString(), serverAddress.ToString());
uint msgId = 0;
Message reqMsg = new Message();
reqMsg.Body = new BodyRequest()
{
FILESIZE = new FileInfo(filepath).Length,
FILENAME = System.Text.Encoding.Default.GetBytes(filepath)
};
reqMsg.Header = new Header()
{
MSGID = msgId++,
MSGTYPE = CONSTANTS.REQ_FILE_SEND,
BODYLEN = (uint)reqMsg.Body.GetSize(),
FRAGMENTED = CONSTANTS.NOT_FRAGMENTED,
LASTMSG = CONSTANTS.LASTMSG,
SEQ = 0
};
TcpClient client = new TcpClient(clientAddress);
client.Connect(serverAddress);
NetworkStream stream = client.GetStream();
MessageUtil.Send(stream, reqMsg); // 클라이언트는 서버에 접속하자마자 파일 전송요청 메시지를 보냄
Message rspMsg = MessageUtil.Receive(stream); // 그리고 서버의 응답을 받습니다.
if (rspMsg.Header.MSGTYPE != CONSTANTS.REP_FILE_SEND)
{
Console.WriteLine("정상적인 서버 응답이 아닙니다.{0}",
rspMsg.Header.MSGTYPE);
return;
}
if (((BodyResponse)rspMsg.Body).RESPONSE == CONSTANTS.DENIED)
{
Console.WriteLine("서버에서 파일 전송을 거부했습니다.");
return;
}
using (Stream fileStream = new FileStream(filepath, FileMode.Open)) // 서버에서 전송요청을 수락했다면, 파일 스트림을 열어 서버로 보낼 준비
{
byte[] rbytes = new byte[CHUNK_SIZE];
long readValue = BitConverter.ToInt64(rbytes, 0);
int totalRead = 0;
ushort msgSeq = 0;
byte fragmented =
(fileStream.Length < CHUNK_SIZE) ?
CONSTANTS.NOT_FRAGMENTED : CONSTANTS.FRAGMENTED;
while (totalRead < fileStream.Length)
{
int read = fileStream.Read(rbytes, 0, CHUNK_SIZE);
totalRead += read;
Message fileMsg = new Message();
byte[] sendBytes = new byte[read];
Array.Copy(rbytes, 0, sendBytes, 0, read);
fileMsg.Body = new BodyData(sendBytes);
fileMsg.Header = new Header()
{
MSGID = msgId,
MSGTYPE = CONSTANTS.FILE_SEND_DATA,
BODYLEN = (uint)fileMsg.Body.GetSize(),
FRAGMENTED = fragmented,
LASTMSG = (totalRead < fileStream.Length) ?
CONSTANTS.NOT_LASTMSG :
CONSTANTS.LASTMSG,
SEQ = msgSeq++
};
Console.Write("#");
MessageUtil.Send(stream, fileMsg); // 모든 파일의 내용이 전송될 때까지 파일 스트림을 메시지에 담아 서버로 보냄
}
Console.WriteLine();
Message rstMsg = MessageUtil.Receive(stream);// 서버에서 파일을 제대로 받았는지에 대한 응답을 받습니다.
BodyResult result = ((BodyResult)rstMsg.Body);
Console.WriteLine("파일 전송 성공 : {0}",
result.RESULT == CONSTANTS.SUCCESS);
}
stream.Close();
client.Close();
}
catch (SocketException e)
{
Console.WriteLine(e);
}
Console.WriteLine("클라이언트를 종료합니다.");
}
}
}