본 문서는
1. 단편화 의미와 발생 이유
2. TCP/IP 통신에서 대량의 데이터를 연속으로 송/수신 시 수신 측 이벤트에서 발생될 수 있는 문제점
2-1. 서버와 클라이언트 통신 환경설정
2-2. 대량의 데이터를 연속으로 송/수신 시 수신 측 이벤트에서 발생하는 문제점
3. 단편화 해결 방안
4. 프로그램 핵심 소스(서버 측 코드 vs 클라이언트 측 코드)
5. 예제 프로그램 사용 방법
순서로 진행합니다.
TCP/IP 통신에서 대량의 데이터를 연속으로 송/수신하는 경우 수신 측 이벤트(OnReceive Handler)에서 데이터의 단편화(Fragmentation) 현상이 발생합니다. 즉, 송신 측에서 보낸 데이터와 수신 측에서 받은 데이터의 총 사이즈를 비교하면 동일하지만 수신 이벤트에서 단편화(Fragmentation)가 발생하여 매회 수신되는 데이터의 사이즈는 다르게 됩니다. 이를 해결하기 위해서는 수신된 데이터의 패킷을 Frame 화할 수 있는 별도의 처리가 필요합니다. SmartX Framework에서는 송/수신 시 STX(시작 구분 코드)와 ETX(마감 구분 코드) 사용하여 패킷을 프레임화하고 데이터 길이와 CRC32를 사용하여 에러 체크를 수행합니다.
1. 단편화 의미와 발생 이유
단편화란 데이터가 전송될 때 전송 데이터가 수신 측 이벤트에 한 번에 전송되지 않고 조각(Fragment)으로 나누어져 전송되는 것을 의미합니다. 단편화 발생 이유는 특정 장비의 MTU(특정 장비에서 한 번에 보낼 수 있는 IP 데이터 그램의 최대 크기)보다 큰 패킷이 들어왔을 때 발생하게 되며 수신되는 데이터는 조각으로 나누어져 수신 받게 됩니다.
2. TCP/IP 통신에서 대량의 데이터를 연속으로 송/수신 시 수신 측 이벤트에서 발생될 수 있는 문제점 2-1. 서버와 클라이언트 통신 환경설정
예시) [서버 – 인트라넷 – 클라이언트], [서버 – 인터넷 – 클라이언트]
2-2. 대량의 데이터를 연속으로 송/수신 시 수신 측 이벤트에서 발생하는 문제점(단편화 발생)
예시) [서버 → 클라이언트] 또는 [클라이언트 → 서버]로 대량의 데이터(1500Byte이상)을 연속(인터벌1ms)로 송신 시 수신 측에서 발생하는 문제점
3. 단편화 해결 방안
SamrtX Framework에서 사용한 해결 방법은 송신 측에서는 보낼 데이터를 STX(시작 구분 코드) + DATA 길이 + CRC32 + DATA + ETX(마감 구분 코드)로 프레임 형식의 프로토콜로 구성하여 송/수신 데이터를 검증할 수 있도록 설계합니다.
[표] 송/수신 데이터의 프레임 구조 예시패킷 | STX | DATA 길이 | CRC32 | DATA | ETX |
---|---|---|---|---|---|
설명 | 시작 구분 코드 | 송/수신 데이터 길이 (DATA의 길이) |
에러 체크 | 전송할 데이터 | 마감 구분 코드 |
예제 예시 | "@" | "00010" | "A80D6516"(16진수) | "ABCDEFGHIJ" (데이터에 STX(@), ETX(;) 문자 없어야 함 |
“;” |
※ 모든 데이터는 Unicode 문자열로 처리됨
수신 측 OnReceiveHandler 이벤트에서 데이터를 수신하고 GetFrameData()에서 데이터의 구조가
Frame 구조로 되어 있는지 확인하여 정상 프레임 구조일 때만 그다음 처리를 진행합니다.
정상 프레임 구조라면 Verification 메서드에서 수신 데이터의 길이와 CRC 에러 검증을 수행하여
에러가 발생하는 경우 관련 에러 메시지를 반환합니다.
4. 프로그램 핵심 소스(서버 측 코드 vs 클라이언트 측 코드)
※ 본 예시는 참고용이며 적용하고자 하시는 프로젝트에서 응용하여 적용하시기 바랍니다.
서버 측 코드(서버 시작) | 클라이언트 측 코드(클라이언트 서버 접속) |
---|---|
private void butStart_Click(object sender, EventArgs e) { // 비동기 통신
} smartTCPMultiServer1.Port = 3377; smartTCPMultiServer1.MaxClient = 30; smartTCPMultiServer1.MaxReceiveBufferSize = 500000; smartTCPMultiServer1.ReceiveTimeout = 3000; if (smartTCPMultiServer1.IsStart == false) { smartTCPMultiServer1.Start();
} |
private void butConnect_Click(object sender, EventArgs e) {
smartTCPClient1.ServerIPAddress = "192.168.0.20";
} smartTCPClient1.Port = 3377; smartTCPClient1.MaxReceiveBufferSize = 500000; smartTCPClient1.ReceiveTimeout = 3000; if (smartTCPClient1.IsConnect == false) { // 서버와 연결
}smartTCPClient1.Connect(); |
서버 측 코드(데이터 송신) | 클라이언트 측 코드(수신 이벤트) |
---|---|
private void smartTimer1_Tick(object sender, EventArgs e) { // Frame Format : STXCODE(1) + LENGTH(5) + CRC(8) + DATA(N) + ETXCODE(1)
} smartTCPMultiServer1. SendStringUnicodeIP("192.168.0.20", STXCODE + m_sSendData.Length.ToString("00000") + m_CRC32Data + m_sSendData + ETXCODE); |
if (strRecData != "") { // m_strReceive의 데이터가 공백이 될 때까지 strFrameData(STXCODE(1) + LENGTH(5) + CRC(8) + DATA(N) + ETXCODE(1)) 구조로 완성하여 출력
} do { strFrameData = GetFrameData(ref m_strReceive);
}if (strFrameData != "") { switch (Verification(strFrameData))
}{ // 정상 수신
}case VERIFYCODE.SUCESS: lblrecvTotal.Text =
// CRC 오류 발생(++recvTotalCnt).ToString(); smartListBox1.AddItem("[" + recvTotalCnt.ToString() + "] " + strFrameData); smartFile1.StringType.WriteBuffer(strFrameData + "\r\n\r\n"); byte[] StrByte = Encoding.Unicode.GetBytes(m_sSendData); m_CRC32Data = SmartX.SmartTCPMultiServer.GetCRC32Gen(StrByte).ToString("X"); // Frame Format : STXCODE(1) + LENGTH(5) + CRC(8) + DATA(N) + ETXCODE(1) // 모든 문자열은 Unicode 문자열로 처리됨 smartTCPClient1.SendStringUnicode(STXCODE + m_sSendData.Length.ToString("00000") + m_CRC32Data + m_sSendData + ETXCODE); lblSentCnt.Text = (++m_iSendCnt).ToString(); break; case VERIFYCODE.CRC_ERROR: lblErrorCnt.Text = (++recvErrorCnt).ToString();
// 수신 데이터 길이 오류 발생lblErrorMesg.Text = "CRC_ERROR"; break; case VERIFYCODE.LENGTH_ERROR: lblErrorCnt.Text = (++recvErrorCnt).ToString();
// 그 외 오류 발생lblErrorMesg.Text = "LENGTH_ERROR"; break; case VERIFYCODE.ERROR: lblErrorCnt.Text = (++recvErrorCnt).ToString();
lblErrorMesg.Text = "ERROR"; break; while (strFrameData != ""); |
수신 측에서 데이터를 프레임 구조로 처리하는 함수(GetFrameData) |
---|
private const string STXCODE = "@"; private const string ETXCODE = ";"; // 프레임(STX+LENGTH+CRC32+데이터+ETX)구조로 만들어질 때까지 ""을 리턴하고 // 프레임 구조로 만들어진 경우 해당 프레임 부분을 리턴하여 프레임을 제외한 나머지 부분은 strInData에 저장 private string GetFrameData(ref string strInData) { int iPosSTX;
} int iPosETX; string strTemp; string strRet; strTemp = strInData; // strTemp에서 맨 처음 발견되는 지정된 유니코드 문자('@')의 0부터 시작하는 인덱스를 iPosSTX에 저장 iPosSTX = strTemp.IndexOf(STXCODE); // strTemp에서 '@'를 발견하지 못한 경우 if (iPosSTX == -1) { return "";
}// strTemp에서 맨 처음 발견되는 지정된 유니코드 문자(';')의 0부터 시작하는 인덱스를 iPosETX에 저장 iPosETX = strTemp.IndexOf(ETXCODE); // strTemp에서 ';'를 발견하지 못한 경우 if (iPosETX == -1) { return "";
}// @와 ;가 모두 있어야만 이 부분 실행됨 // iPosSTX부터 (iPosETX - iPosSTX) + 1까지의 문자를 strRet에 저장 strRet = strTemp.Substring(iPosSTX, (iPosETX - iPosSTX) + 1); // iPosSTX부터 (iPosETX - iPosSTX) + 1까지의 문자를 삭제 // strTemp - strRet 나머지를 strInData에 저장. GetFrameData(ref string strInData)호출 시 인자로 전달 strInData = strTemp.Remove(iPosSTX, (iPosETX - iPosSTX) + 1); // @와 ;가 들어있는 완전한 프레임을 반환 return strRet; |
수신 측에서 데이터를 검증(LENGTH, CRC)하는 메서드 |
---|
public enum VERIFYCODE { SUCESS = 0, // 성공
}LENGTH_ERROR = 1, // 길이 에러 CRC_ERROR = 2, // CRC 에러 ERROR = 3, // 그 외 에러 // Frame Format : STX(1) + LENGTH(5) + CRC(8) + DATA(N) + ETX(1) public VERIFYCODE Verification(string strFrameData) { int iLength_Read; // 데이터 길이
} int iLength_Cal; // 데이터 길이 검증 string strCRC_Read; // 수신 프레임에서 추출된 CRC 저장 int iCRC_Read; // strCRC_Read(16진수) → int형으로 변환 int iCRC_Cal; // CRC 값을 계산하고 int형으로 변환 string strData; byte[] chData; try { // 수신 데이터에서 데이터의 길이 추출
}iLength_Read = Convert.ToInt32(strFrameData.Substring(1, 5)); // 수신 데이터의 데이터의 길이 계산 iLength_Cal = strFrameData.Length - 15; if (iLength_Read != iLength_Cal) { // 길이 오류
}return VERIFYCODE.LENGTH_ERROR; // 수신된 데이터에서 실제 데이터만 추출 strData = strFrameData.Substring(14, strFrameData.Length - 15); // CRC 추출 strCRC_Read = strFrameData.Substring(6, 8); // 16진수를 int형으로 변환하여 iCRC_Read에 저장 iCRC_Read = Convert.ToInt32(strCRC_Read, 16); // String형을 Unicode 타입의 바이트 배열로 변환 chData = Encoding.Unicode.GetBytes(strData); // CRC 값을 계산 int형으로 변환하여 iCRC_Cal에 저장 iCRC_Cal = (int)SmartX.SmartTCPMultiServer.GetCRC32Gen(chData); catch (Exception) { return VERIFYCODE.ERROR;
}// 수신 데이터의 CRC32 값과 계산하여 만든 CRC32 값이 일치 안 한다면 if (iCRC_Read != iCRC_Cal) { return VERIFYCODE.CRC_ERROR;
}return VERIFYCODE.SUCESS; |
서버 측 코드(서버 종료) | 클라이언트 측 코드(클라이언트 종료) |
---|---|
private void butExit_Click(object sender, EventArgs e) { if (smartTCPMultiServer1.IsStart == true)
} { smartTCPMultiServer1.Stop();
}smartForm1.Close(); |
private void butExit_Click(object sender, EventArgs e) { if (smartTCPClient1.IsConnect == true)
} { // 서버와 연결 종료
}smartTCPClient1.Close(); smartForm1.Close(); |
5. 예제 프로그램 사용 방법
- [STEP-1] 클라이언트 프로그램의 소스 코드에서 Server의 IP 주소를 입력합니다.
서버를 열거나 접속하기 전에 클라이언트 프로그램 코드의 ServerIPAddress 변수에 서버 IP주소를 입력하세요.
- [STEP-2] 서버 프로그램의 Server – Start 버튼을 클릭하여 서버를 시작합니다.
- [STEP-3] 클라이언트 프로그램의 Server Connect를 클릭하여 클라이언트를 서버에 접속합니다.
- [STEP-4] 서버 프로그램의 SendCount(전송할 데이터 개수)를 설정하고 TimerInterval(전송할 데이터간 인터벌) 값을 설정합니다.
- [STEP-5] 서버 프로그램의 SendToClient를 클릭하여 클라이언트로 데이터 전송을 시작합니다.
- [STEP-6] 클라이언트 측 수신 데이터 확인
데이터 전송이 모두 완료된 이후 클라이언트 프로그램의 FileWrite를 클릭하여 서버로부터 수신받은 데이터를 파일(Test1.txt)로 저장하고 해당 파일을 열어서 STXCODE('@')의 개수와 ETXCODE('!')의 개수가 서버 측에서 보낸 Send Count 개수와 일치하는지 확인하시면 됩니다.
데이터 송신이 정상인 경우) 서버 측 SentCount(1000개) = 클라이언트 STXCODE('@')의 개수 = 클라이언트 ETXCODE('!')의 개수가 일치하면 됩니다. - [STEP-7] 서버 측 수신 데이터 확인
서버 프로그램의 FileWrite를 클릭하여 클라이언트로부터 수신받은 데이터를 파일(Test1.txt)로 저장하고 해당 파일을 열어서 STXCODE('@')의 개수와 ETXCODE('!')의 개수가 클라이언트에서 보낸 Sent Count 개수와 일치하는지 확인하시면 됩니다.