호출자 정보
class CallerInfo
{
public static void Main()
{
LogMessage("test");
}
//호출자 정보는 '호출하는 측의 정보'를 메서드의 인자로 전달하는 것이다.
//호출자 정보 특성이 명시된 매개변수는 선택적 매개변수 형식이여야 한다.
//컴파일 시점에 값이 치환되어 빌드된다.
static void LogMessage(string text,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine("텍스트 : " + text); //"test" 출력
Console.WriteLine("이 메서드를 호출하는 메서드 이름" + memberName); //Main 출력
Console.WriteLine("호출 파일 경로" + filePath); //파일 경로 출력
Console.WriteLine("메서드가 호출된 라인 번호" + lineNumber); //10 출력
}
}
비동기 호출
async/await 예약어
비동기 호출에 await 예약어가 함께 쓰이면 C# 컴파일러는 이를 인지하고 그 이후의 코드를 묶어서 비동기 호출이 끝난 다음에 실행되도록 코드를 변경해서 컴파일한다. async 예약어가 있으면 컴파일러는 await 예약어를 예약어로 인식한다. async 예약어가 없으면 그냥 식별자로 인식한다.
class AsyncAwaitTest
{
//async 예약어가 없이 await를 사용할 수 없다.
private static async void AwaitRead()
{
using (FileStream fs = new FileStream("test.log", FileMode.Open))
{
byte[] buffer = new byte[fs.Length];
Console.WriteLine("실행 전의 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine("실행 후의 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
//ReadAsync 이하의 처리는 스레드 풀 스레드가 담당하기 때문에 스레드 id가 다르게 나온다.
//이하의 라인은 ReadAsync 비동기 호출이 완료된 후 호출
string txt = Encoding.UTF8.GetString(buffer);
Console.WriteLine(txt);
}
}
public static void Main()
{
AwaitRead();
Console.WriteLine("메인 함수 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
//이게 2번째로 실행된다.
Console.ReadLine();
}
}
class BCLAsyncTest //WebClient 비동기 호출 async/await 예제
{
public static void Main()
{
AwaitDownloadString();
Console.ReadLine();
}
private static async void AwaitDownloadString()
{
WebClient wc = new WebClient();
wc.Encoding = Encoding.UTF8;
//DownloadStringAsync보다 더 간편하게 이용 가능.
string text = await wc.DownloadStringTaskAsync("http://www.naver.com");
Console.WriteLine(text);
}
}
class BCLAsyncTest2 //TCP 서버 비동기 통신 예제
{
public static void Main()
{
TcpListener listener = new TcpListener(IPAddress.Any, 11200);
listener.Start();
while (true)
{
//연결 요청을 받아들인다.
var client = listener.AcceptTcpClient();
ProcessTcpClient(client);
}
}
//NetworkStream 클래스의 ReadAsync와 WriteAsync를 이용하면 간단하게 비동기 통신 구현 가능
private static async void ProcessTcpClient(TcpClient client)
{
NetworkStream ns = client.GetStream();
byte[] buffer = new byte[1024];
int received = await ns.ReadAsync(buffer, 0, buffer.Length);
string txt = Encoding.UTF8.GetString(buffer, 0 ,received);
byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello : " + txt);
await ns.WriteAsync(sendBuffer, 0, sendBuffer.Length);
ns.Close();
}
}
Task, Task<TResult> 타입
Async 메서드의 반환값은 모두 Task 또는 Task<TResult> 유형이다. Task 타입은 반환값이 없는 경우 사용되고, Task<TResult> 타입은 TResult 형식 매개변수로 지정된 반환값이 있는 경우로 구분된다.
class TaskType
{
public static void Main()
{
//스레드 풀에서
ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine("process workitem"); }, null);
Task task1 = new Task(() => { Console.WriteLine("process taskitem"); });
task1.Start();
Task task2 = new Task((obj) => { Console.WriteLine("process taskitem(obj)"); }, null);
task2.Start();
task1.Wait(); //task1의 작업이 끝날 때까지 현재 스레드를 대기한다.
task2.Wait();
//task 객체를 생성할 필요 없이 바로 작업 시작 가능.
Task.Factory.StartNew(() => { Console.WriteLine("process taskitem StartNew"); });
//Task<TResult> 타입은 값을 반환할 수 있다.
Task<int> task3 = new Task<int>(() =>
{
Random rand = new Random((int)DateTime.Now.Ticks);
return rand.Next();
}
);
task3.Start();
task3.Wait();
Console.WriteLine("무작위 숫자 값 : " + task3.Result);
//StartNew<TResult> 도 반환.
Task<int> taskReturn = Task.Factory.StartNew<int>(() => 255);
taskReturn.Wait();
Console.WriteLine(taskReturn.Result);
}
}
Async 메서드가 아닌 경우의 비동기 처리
class TaskAsync
{
//비동기로 처리할 ReadAllTextAsync라는 메서드를 만들어서 파일 경로를 넘겨준다.
private static async void AwaitFileRead(string filePath)
{
string fileText = await ReadAllTextAsync(filePath);
Console.WriteLine(fileText);
}
//Task를 이용하여 넘겨받은 파일 경로로 들어가 텍스트를 읽고 리턴한다.
static Task<string> ReadAllTextAsync(string filePath)
{
return Task.Factory.StartNew(() =>
{
return File.ReadAllText(filePath);
});
}
public static void Main()
{
string filePath = "test.log";
//비동기로 처리되므로 바로 다음 줄 실행
AwaitFileRead(filePath);
Console.ReadLine();
}
}
비동기 호출의 병렬 처리
//비동기 처리 담당 메서드
private static async void DoAsyncTask()
{
var task3 = Method3Async();
var task5 = Method5Async();
//task3, task5가 끝날 때까지 대기한다.
await Task.WhenAll(task3, task5);
//그 이후 실행
Console.WriteLine(task3.Result + task5.Result);
}
//3초 후 3리턴
private static Task<int> Method3Async()
{
return Task.Factory.StartNew( () =>
{
Thread.Sleep(3000);
return 3;
});
}
//5초 후 5리턴
private static Task<int> Method5Async()
{
return Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
return 5;
});
}
public static void Main()
{
DoAsyncTask();
Console.WriteLine("먼저 출력되고, 비동기 값 나중에 출력됨.");
Console.ReadLine();
}
}
확장메서드
Static 메서드를 인스턴스 메서드처럼 클래스명 없이 바로 호출할 수 있는 메서드이다. 메서드의 첫 번째 인자에 확장하려는 타입을 넣고 그 앞에 this 키워드를 붙이면 된다.
//현재 날짜로부터 n일을 뺀 날짜를 구해주는 메서드를 가진 헬퍼 클래스.
public static class DateTimeHelper
{
//기존 헬퍼 메서드
public static DateTime MinusDays(DateTime dt, int days)
{
DateTime d = dt.AddDays(-days);
return d;
}
//확장 메서드
//확장하려는 타입이 DateTime이고 그 앞에 this를 붙였다.
public static DateTime MinusDaysEx(this DateTime dt, int days)
{
DateTime d = dt.AddDays(-days);
return d;
}
}
class ExtensionMethod
{
public static void Main()
{
//DateHelper라는 클래스 이름과 함께 MinusDays 메서드를 호출해야한다.
//날짜 연산의 대상이 되는 current 객체를 넘겨줘야 한다.
DateTime current = DateTime.Now;
DateTime dt1 = DateTimeHelper.MinusDays(current, 3);
Console.WriteLine(dt1.ToString());
//확장 메서드를 이용하면 헬퍼 클래스의 이름을 기억해서 호출할 필요가 없다.
//대상이 되는 객체를 인자로 넘겨줄 필요가 없다.
DateTime dt2 = current.MinusDaysEx(3);
Console.WriteLine(dt2.ToString());
Console.ReadLine();
}
}
람다 식
코드로서의 람다 식
익명 메서드를 이용하면 델리게이트에 전달되는 메서드가 일회성으로 사용될 때 간편하게 나타낼 수 있다.
//익명 메서드를 이용한 예시
Thread thread = new Thread(
delegate(object obj)
{
Console.WriteLine("익명 메서드로 호출!");
});
위의 코드를 람다 식을 이용하면 더욱 간편하게 표기가능하다.
Thread thread = new Thread(
//object obj 처럼 더 명확하게 나타낼 수 있다. 전달되는 매개변수가 없으면 ()로 표기.
(obj) =>
{
Console.WriteLine("람다 식으로 호출!");
});
매개변수가 2개 이상이면 다음과 같이 표기한다.
delegate int? MyDivide(int a, int b);
MyDivide myFunc = (a,b) =>
{
if (b == 0)
{
return null;
}
return a / b;
};
return 문을 생략도 가능하다.
delegate int MyAdd(int a, int b);
MyAdd myFunc = (a, b) => a + b;
람다 식을 위한 전용 델리게이트
람다식은 델리게이트랑 대응된다. 하지만 일회성으로 사용되는 간단한 람다 식을 쓸 때마다 델리게이트를 정의해야 한다면 불편할 것이다. 그래서 자주 사용되는 델리게이트 형식을 제공한다.
public delegate void Action<T>(T obj);
-> 반환값이 없는 델리게이트로서 T 형식 매개변수는 입력될 인자 1개의 타입을 지정.
public delegate TResult Func<TResult>();
-> 반환값이 있는 델리게이트로서 TResult 형식 매개변수는 반환될 타입을 지정.
class ActionFunc
{
public static void Main()
{
Action<string> printText =
(txt) =>
{
Console.WriteLine("매개변수로 입력된 텍스트는 : " + txt);
};
printText("좋은 하루입니다!");
//Func<T, TResult>(T arg);
Func<int, int> square = (num) => num* num;
Console.WriteLine(square(5));
}
}
인자를 16개까지 받을 수 있는 Action과 Func가 미리 정의되어 있다.
컬렉션과 람다 식
class CollectionLambda
{
public static void Main()
{
List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
//ForEach 메서드를 이용해 간단하게 컬렉션에서의 연산 가능.
//컬렉션에서 요소를 하나씩 꺼내서 element로 전달. Action<T> 연산 수행.
list.ForEach((element) => { Console.WriteLine(element + " * 2 == " + (element * 2)); });
Array.ForEach(list.ToArray(), (element) => { Console.WriteLine(element + " * 2 == " + (element * 2)); });
//FindAll 메서드를 이용하면 특정 조건에 맞는 요소만 고를 수 있다.
List<int> evenList = list.FindAll((element) => element % 2 == 0);
evenList.ForEach(Console.WriteLine);
//Count 메서드를 이용해서 특정 조건을 만족하는 요소들의 개수를 셀 수 있다.
int count = list.Count((element) => element > 3);
Console.WriteLine("3보다 큰 요소의 개수 : " + count);
}
}
LINQ 메서드식 표현
LINQ 쿼리는 컴파일 될 때 LINQ 확장 메서드로 자동 변경되서 호출된다. 다음의 세 가지 코드는 완전히 동일한 역할을 한다.
표현 | 코드 |
---|---|
LINQ 표현 | from person in people select person; |
확장 메서드 표현 | people.Select( (elem) => elem ); |
일반 메서드 표현 | IEnumerable SelectFunc(List people) { foreach (var item in people) { yield return item; } } |
LINQ 확장 메서드 중 Where()과 FirstOrDefault()의 예제 코드이다.
//public static IEnumerable<TSource> Where<TSource>(
//this IEnumerable<TSource> source, Func< TSource, bool> predicate)
//Where 메서드는 IEnumerable<T> 인터페이스를 지원하는 모든 타입에 사용 가능하다.
//predicate가 True를 리턴하는 요소만 모아서 IEnumerable<T>로 열거형을 반환한다.
IEnumerable<int> enumList = list.Where((element) => element % 2 == 0);
Array.ForEach(enumList.ToArray(), (element) => { Console.WriteLine(element); });
var enumList2 = list.Where((element) => element % 2 == 1);
enumList2.ToList<int>().ForEach(n => Console.WriteLine("홀수는 = " + n));
//public static TSource FirstOrDefault<TSource>(
//this IEnumerable<TSource> source)
//FirstOrDefault 메서드는 조건식에 맞는 처음 요소를 찾는 경우에 사용한다.
//조건에 맞는 요소가 여러 개일 경우 첫 요소만 반환하며, 하나도 없을 경우는 기본값(보통 null)을 반환한다.
List<string> stringList = new List<string>() { "Apple", "AOA", "Banana", "Grape" };
var result = stringList.Where((element) => element.StartsWith("A")).FirstOrDefault();
Console.WriteLine("A로 시작하는 첫 번째 요소는 : " + result);
var result2 = stringList.Where((element) => element.StartsWith("C")).FirstOrDefault();
//그냥 null로 체크하는 것 보다는 default(type)으로 체크하는게 더 안전하다.
//밸류 타입의 경우는 null로 체크하면 에러를 반환한다.
if (result2 == default(string))
{
Console.WriteLine("C로 시작하는 요소가 없습니다.");
}
else
{
Console.WriteLine("C로 시작하는 첫 번째 요소는 : " + result2);
}
질문사항
-
await를 사용하면 항상 쓰레드가 자동으로 만들어지는가?
await가 보장하는 것은, Task가 UI Thread에 돌던지, Worker thread에서 돌던지 상관없이 Task 완료 후 await 이후의 실행문들을 디폴트로 원래 await를 실행하기 전의 Thread에서 실행하도록 보장하는 것이다.
하지만 내가 실험하는 환경은 콘솔 어플리케이션이다. 콘솔 어플리케이션은 GUI와 달리 Thread pool SynchronizationContext를 가지기 때문에 await 이후의 부분이 thread pool thread에 스케쥴링된다. 그래서 이곳의 코드를 보면 실행 전, 후의 ThreadId가 다르다. 이것 때문에 await가 스레드를 자동으로 만드는 것처럼 보였던 것이다.
async/await 관련 글
SynchronizationContext 관련 글MSDN 참고
The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. -
await Task.Delay() 했을때와 await fs.ReadAsync() 했을 때 blocking 동작이 다른 이유는 무엇인가?
class AsyncTimeTest { private static async void AsyncRead(int i) { using (FileStream fs = new FileStream("test.log", FileMode.Open, FileAccess.Read)) { byte[] buffer = new byte[fs.Length]; await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine("ReadAsync({0})", i);
string txt = Encoding.UTF8.GetString(buffer);
Console.WriteLine(txt);
}
}
private static async void TaskDelay(int i)
{
await Task.Delay(1000);
Console.WriteLine("Task.Delay({0})", i);
}
public static void Main()
{
Console.WriteLine("메인 시작 시간 : " + DateTime.Now.Ticks / 10000);
for (int i = 0; i < 100; i++)
{
TaskDelay(i);
//AsyncRead(i);
Console.WriteLine(i + "번 돌았다!");
//위 출력이 없으면 Delay(1)일 때 실행되지 않음.
//Delay(1000)으로 하면 i번 돌았다! 만 출력됨.
//AsyncRead는 평균 20번 정도 실행됨.
//그러므로 await로 인한 대기 시간은 ReadAsync < Delay(1) 이다. (test.log을 읽을때 기준)
//ReadAsync와 Delay의 blocking 동작이 다른게 아니라 1번만 실행했을 때,
//Main()이 종료되기 전에 출력되기도 하고, 출력되기 전에 Main()이 끝나기도 해서 동작이 달라보였던 것.
}
//Console.ReadLine();
Console.WriteLine("메인 끝 시간 : " + DateTime.Now.Ticks / 10000);
}
}
3. **한 메소드 안에 await가 두개 이상이면 어떻게 동작하는가?**
```c#
class MultiAwaitTest
{
private async void Run()
{
await Task.Run(() => Do1()); //Do1의 실행이 끝나야 다음으로 넘어간다.
Console.WriteLine("중간");
await Task.Run(() => Do2());
Console.WriteLine("끝"); //Do2가 끝나야 실행된다.
}
private void Do1()
{
Console.WriteLine("DO1");
for (int i = 0; i < 10; i++) Console.WriteLine(i + "번");
; }
private void Do2()
{
Console.WriteLine("DO2");
for (int i = 0; i < 10; i++) Console.WriteLine(i + "번");
}
public static void Main()
{
MultiAwaitTest test = new MultiAwaitTest();
test.Run(); //첫번째 await를 만나면 다음으로 넘어간다.
Console.WriteLine("test.Run() 다음 줄 실행"); //Do1과 Do2의 실행 속도에 따라서 결과가 달라진다.
Console.ReadLine();
}
}
-
Task에 대한 스터디 보강 (thread는 리턴값이 있는가? Task와 thread 차이는 무엇인가? 왜 Task 타입을 정의했을까?)
다시 한 번 정리하기. -
Random 클래스의 seed 역할은 무엇인가?
그냥 Random() 메서드가 발생시키는 난수는 항상 규칙적이다. 이를 의사 난수(pseudo-random number) 라고 한다. 의사 난수는 seed 값에 따라서 결과값이 달라지는데, 이 seed 값이 같으면 매번 같은 결과값이 나온다. 그래서 보통 seed 값에 시간을 넣어 Random() 결과가 시간의 흐름에 따라 계속해서 다르게 출력되는 식으로 이용한다. 이와 같은 방법으로 우리가 흔히 생각하는 의미(실행 할 때마다 값이 다르게 나오는)의 난수를 구현한다. -
람다식의 변수 스코프는?
람다 식은 람다 함수를 정의하는 메서드 범위 내에 있거나 람다 식을 포함하는 형식 범위 내에 있는 외부 변수를 참조할 수 있습니다. (MSDN)
Free Variable(밑의 코드에서는 j)가 스택에서 사라지는 현상을 막기위해서 C# 컴파일러는 자체적으로 nested class를 만들어서 Free Variable을 필드, 람다 식을 메서드로 저장한다. 그러면 힙에 저장되기 때문에 계속해서 상태가 유지될 수 있다. 더 이상 참조하는 객체가 없으면 이 Nested class는 GC에 의해 해제된다.
class LambdaScope { delegate bool D(); delegate bool D2(int i);
D del;
D2 del2;
public void TestMethod(int input)
{
int j = 0;
//람다식 외부의 변수인 j에 접근이 가능하다.
del = () => { j = 10; return j > input; };
del2 = (x) => { return x == j; };
//del이 호출되지 않아서 여전히 j = 0이므로 False 출력.
Console.WriteLine("del호출 전 del2 리턴값 : " + del2(10));
//del이 호출되지 않았으므로 j = 0이다.
Console.WriteLine("j = {0}", j);
//del이 호출되면서 j = 10이 들어간다.
bool boolResult = del();
//j = 10, b = True가 출력된다.
Console.WriteLine("j = {0}, b = {1}", j, boolResult);
}
public static void Main()
{
LambdaScope test = new LambdaScope();
test.TestMethod(5);
bool result = test.del2(10);
//True 출력.
Console.WriteLine("del호출 후 del2 리턴값 : " + result);
Console.ReadKey();
}
}
7. **IEnumerable<T> 변수를 foreach 키워드로 순회하도록 변경 바랍니다.**
```c#
IEnumerable<int> enumList = list.Where((element) => element % 2 == 0);
foreach (int i in enumList) Console.WriteLine(i);
var enumList2 = list.Where((element) => element % 2 == 1);
foreach (int i in enumList2) Console.WriteLine("홀수는 = " + i);
```
8. **Where().FirstOrDefault() 하지 말고 곧장 FirstOrDefault() 할 수 없나요?**
```c#
List<string> stringList = new List<string>() { "Apple", "AOA", "Banana", "Grape" };
var result = stringList.FirstOrDefault((element) => element.StartsWith("A"));
Console.WriteLine("A로 시작하는 첫 번째 요소는 : " + result);
var result2 = stringList.FirstOrDefault((element) => element.StartsWith("C"));
if (result2 == default(string))
{
Console.WriteLine("C로 시작하는 요소가 없습니다.");
}
else
{
Console.WriteLine("C로 시작하는 첫 번째 요소는 : " + result2);
}
```
9. **Network endian 스터디 보강**
**Big-Endian**
상위 바이트의 값을 작은 번지수부터 저장하는 방식.
빅 엔디언은 소프트웨어의 디버그를 편하게 해 주는 경향이 있다. 사람이 숫자를 읽고 쓰는 방법과 같기 때문에 디버깅 과정에서 메모리의 값을 보기 편하기 때문이다.
**Little-Endian**
상위 바이트의 값을 큰 번지수부터 저장하는 방식.
리틀 엔디언은 메모리에 저장된 값의 하위 바이트들만 사용할 때 별도의 계산이 필요 없다는 장점이 있다.
하지만 두 엔디언 사이에 사실상 성능의 차이는 없다.
**호스트 바이트 순서**
어떤 시스템(motorola, sun사 기계)은 Big-Endian, 어떤 시스템(intel사)은 Little-Endian을 사용하므로 일정하지 않다.
데이터 표현 방식이 다르므로 서로 다른 엔디안을 사용하는 플랫폼 사이에 데이터가 오고가면 문제가 발생한다.
**네트워크 바이트 순서**
그래서 네트워크를 통해서 데이터를 전송할 때에는 데이터의 배열을 Big-Endian 기준으로 변경해서 송수신하기로 하였다.
네트워크 바이트 순서는 Big-Endian이다.
**바이트 순서 변환 함수**
※ 어느 시스템에서 작동할지 모르기 때문에 반드시 사용해야 한다.
unsigned short htons(unsigned short); // 호스트 -> 네트워크 short
unsigned short ntohs(unsigned short); // 네트워크 -> 호스트 short
unsigned long htonl(unsigned long); // 호스트 -> 네트워크 long
unsigned long ntohl(unsigned long); // 네트워크 -> 호스트 long
C#에서는
IPAddress.HostToNetworkOrder() 나 IPAddress.NetworkToHostOrder()를 쓰면 된다.
숫자 26을 바이트 배열로 만들때
byte[] buffer = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(26));
'h' : host byte order
'n' : network byte order
's' : short(16비트)
'l' : long(32비트)
10. **ContinueWhenAny 메소드 스터디 보강**
11. **stread.Read 메소드를 while 루프로 여러번 읽는 이유는? (TCP stream 이야기)**
TCP는 분리된 메시지를 보내는 것이 아니라, 데이터를 바이트 스트림의 형태로 나눠서 보내기 때문에, 각각의 읽기에서 얼마나 많이 읽혀졌는지를 예측할 수 없다. 그래서 받으려 하는 길이가 0이 될때까지 반복해서 받아야한다.
TCP 통신에서 한 번에 보낼 수 있는 데이터의 양은 정해져있다. 송신속도가 수신속도보다 빠르면 packet drop이 발생해 데이터가 손실될 수 있기 때문에 한 번에 보낼 수 있는 양을 제한해서 여러번 보내는 방식을 취한다. 이를 Flow Control(흐름 제어)라고 한다. 어플리케이션에서 Read 시스템 콜을 호출하면 , 커널 영역의 receive socket buffer에 있는 데이터를 유저 공간의 메모리로 복사하고, 복사한 데이터는 버퍼에서 지운다. 그러면 다시 TCP는 receive window를 증가시키고 ACK 메세지를 보낸다.
원하는 길이만큼 데이터를 받을때까지 위 과정을 반복해야하기 때문에, while 루프로 remainLength 만큼 read했는지 체크하며 반복하는 것이다.
**TCP/IP의 특성**
1. Connection oriented
두 개 엔드포인트(로컬, 리모트) 사이에 연결을 먼저 맺고 데이터를 주고받는다. TCP 연결 식별자는 <로컬 IP 주소, 로컬 포트번호, 리모트 IP 주소, 리모트 포트번호> 형태이다.
2. Bidirectional byte stream
양방향 데이터 통신을 하고, 바이트 스트림을 사용한다.
3. In-order delivery
송신자가 보낸 순서대로 수신자가 데이터를 받는다. 데이터의 순서를 표시하기 위해 32-bit 정수 자료형을 사용한다.
4. Reliability through ACK
데이터를 송신하고 수신자로부터 ACK를 받지 않으면, 송신자 TCP가 데이터를 재전송한다.
5. Flow control
송신자는 수신자가 받을 수 있는 만큼 데이터를 전송한다. 송신 속도보다 수신 속도가 빠를 경우에는 문제가 없지만, 반대의 경우에는 문제가 발생한다. 수신측의 제한된 용량을 넘어서면 데이터가 손실될 수 있기 때문이다. 때문에 수신자는 자신이 받을 수 있는 바이트 수 (사용하지 않은 버퍼 크기, receive window)를 송신자에게 전달한다. 송신자는 수신자 receive window가 허용하는 바이트 수만큼 데이터를 전송한다.
6. Congestion control
네트워크 정체를 방지하기 위해 receive window와 별도로 congestion window를 사용하는데 이는 네트워크에 유입되는 데이터양을 제한하기 위해서이다. Receive window와 마찬가지로 congestion window가 허용하는 바이트 수만큼 데이터를 전송한다. Flow control과 달리 송신자가 단독으로 구현한다.
<br />
**데이터 송신 시 TCP/IP 네트워크 스택의 레이어 별 동작과정**

네트워크 스택은 크게 유저, 커널, 디바이스 영역으로 나눌 수 있다. 유저와 커널 영역의 작업은 CPU가 수행한다. 그리고 디바이스 영역과 구별하기 위해 유저와 커널 영역은 호스트라고 부른다. 디바이스는 패킷을 송수신하는 NIC(Network Interface Card)이다. 흔히 랜카드라고 부른다.
먼저, 유저 영역에서 어플리케이션이 전송할 데이터를 만들고 write 시스템 콜을 호출해서 데이터를 보낸다. 그럼 미리 생성/연결된 소켓을 통해서 데이터 송신을 요청한다. 시스템 콜이 호출되면 커널 영역으로 전환된다.
리눅스나 유닉스 같은 POSIX 계열 운영체제에서는 소켓을 파일의 한 종류로 보고, file descriptor로 어플리케이션에 노출한다. 파일 레이어는 이 file descriptor가 유효한지 확인만 하고 소켓 함수를 호출한다.
커널 소켓은 send socket buffer와 receive socket buffer 두 개의 버퍼를 가지고 있다. Write 시스템 콜이 호출되면 유저 영역의 데이터가 커널 메모리로 복사되고, send socket buffer의 뒷부분에 추가된다. 기존에 보내고 있던 데이터 뒤에 붙여서 전송 순서를 지키기 위함이다. 다음으로 TCP를 호출한다.
소켓과 연결된 TCB(TCP Control Block) 구조체가 있다. TCB에는 TCP 연결 처리에 필요한 정보가 있다. TCB에는 connection state, receive window, congestion window, sequence 번호, 재전송 타이머 등이 있다.
TCP는 현재의 TCP State가 데이터 전송이 가능하면, 새로운 TCP Segment를 만든다. Flow Control과 같은 이유로 데이터 전송이 불가능하면 시스템 콜이 끝나고, 유저모드로 되돌아간다.
TCP Segment에는 TCP 헤더와 페이로드가 있다. 페이로드에는 send socket buffer에 있는 ACK를 받지 않은 데이터가 담겨있다. 페이로드의 최대 길이는 receive window, congestion window, MSS(Maximun Segment Size) 중 최대 값이다.
그리고 TCP checksum을 계산한다. 이 checksum 계산에서는 pseudo 헤더 정보(IP 주소들, segment 길이, 프로토콜 번호)를 포함시킨다.
생성된 TCP segment는 IP 레이어로 내려간다. IP 레이어에서는 TCP Segment에 IP 헤더를 추가하고 IP 라우팅을 한다. IP 레이어에서 IP 헤더 checksum을 계산하여 덧붙인 후, Ethernet 레이어로 데이터를 보낸다.
Ethernet 레이어는 ARP(Address Resolution Protocol)를 사용해서 전송될 장비 MAC 주소를 찾는다. 그리고 Ethernet 헤더를 패킷에 추가한다. 이때 Ethernet 표준에 따라 IFG(Inter-Frame Gap), preamble, 그리고 CRC를 패킷에 추가한다. IFG, preamble은 패킷의 시작을 판단하기 위해 사용하고(네트워킹 용어로는 framing), CRC는 데이터 보호를 위해 사용한다(TCP, IP checksum과 같은 용도이다). Ethernet 헤더까지 붙으면 호스트 패킷이 완성된다.
IP 라우팅을 하면 전송될 장비의 IP 주소와 해당 IP로 패킷을 전송할 때 사용하는 인터페이스(NIC)를 알게된다. 그러면 드라이버는 그 NIC를 호출한다. 드라이버는 NIC 제조사가 정의한 드라이버-NIC 통신 규약에 따라 패킷 전송을 요청한다.
NIC는 패킷 전송을 요청받고, 메인 메모리에 있는 패킷을 자신의 메모리로 복사하고, 네트워크 선으로 전송한다. 그리고 패킷 전송이 완료되면 NIC는 호스트 CPU에 인터럽트를 발생시킨다. 그럼 운영체제는 인터럽트 핸들러를 호출한다. 인터럽트 핸들러는 전송된 패킷(송신 결과)을 운영체제에 반환한다.
<br />
**데이터 수신 시 TCP/IP 네트워크 스택의 레이어 별 동작과정**

패킷이 외부에서 도착하면 NIC가 패킷을 자신의 메모리에 기록한다. CRC 검사로 패킷에 손상이 없는지 확인하고, 호스트의 메모리 버퍼로 전송한다. 이 메모리 버퍼는 드라이버가 커널에 요청하여 패킷 수신용으로 미리 할당한 메모리이다. 이 버퍼가 없으면 NIC는 패킷을 버릴 수 있다. 이를 packet drop이라하고, packet drop을 방지하기 위해 흐름 제어를 한다.
패킷을 호스트 메모리로 전송한 후 NIC가 운영체제에 인터럽트를 보낸다. 드라이버는 수신된 패킷을 보고 자신이 처리할 수 있는 패킷인지 검사한다. 이때까지는 제조사가 정의한 드라이버-NIC 통신 규약을 사용한다.
드라이버가 상위 레이어로 패킷을 전달할 때, 운영체제게 이해할 수 있도록 패킷을 운영체제가 사용하는 패킷 구조체로 포장한다. 드라이버는 포장한 패킷 구조체를 상위 레이어로 전달한다.
Ethernet 레이어에서도 패킷이 올바른지 검사한다. 패킷이 올바르면 Ethernet 헤더를 제거하고 IP 레이어로 패킷을 전달한다.
IP 레이어에서도 IP 헤더 checksum을 확인해서 패킷이 올바른지 검사한다. IP 라우팅을 해서 패킷을 현재 장비에서 처리해야 하는지, 다음 장비로 전송해야 하는지 판단한다. 현재 장비에서 처리해야 하면, IP 헤더를 제거하고 TCP 레이어로 패킷을 전달한다.
TCP 레이어에서도 checksum을 확인해서 패킷이 올바른지 검사한다. 그 다음 TCB를 찾는다. TCB를 찾으면 프로토콜을 수행해서 받은 패킷을 처리한다. 데이터를 해당 TCB에 속한 소켓의 receive socket buffer에 추가한다. 그 후 TCP 상태에 따라서 새로운 TCP 패킷(ACK 등)을 전송할 수 있다. 이렇게 수신 패킷 처리 과정이 끝난다.
이후 어플리케이션이 read 시스템 콜을 호출하면 커널 영역으로 전환되고, socket buffer에 있는 데이터를 유저 공간의 메모리로 복사해 간다. 복사가 끝나면 복사한 데이터는 socket buffer에서 제거하고 TCP를 호출한다. TCP는 socket buffer에 새로운 공간이 생겼기 때문에 receive window를 증가시킨다. 그리고 프로토콜 상태에 따라서 패킷을 전송한다.
<br />
12. **LINQ 결과인 IEnumerable<T>를 for 문으로 루프 돌때, breakpoint 걸어서 확인해보기**


for each의 in이 실행될 때, 위의 Where문의 조건이 참, 거짓인지를 리턴하는 익명 메서드가 호출된다. 해당 요소가 조건에 맞는지 검사해서 참이면 all.Current에 그 요소가 저장되고 아니면 건너뛴다. Current에 저장된 요소는 item로 전달되어 정상적으로 출력된다.