POSIX계열과 윈도 소켓 라이브러리의 차이점은
네트워크 계층과 전송 계층 패킷을 접근할 수 있다.
네트워크 계층에선 발신지 주소와 목적지 주소가 필요하고,
전송 계층에선 발신지 포트와 목적지 포트가 필요하다.
TCP는 신뢰성 보장으로 인해, 데이터를 주고받기 위해 두 호스트 사이의 연결을 맺어 두어야 한다.
또한 누락된 패킷을 재전송하기 위해 상태 정보를 유지하고 저장해 놔야한다.
버클리 소켓 API에서는 socket그자체에 그 연결 정보를 기록하므로 각 TCP클라이언트마다 소켓을
만들어 둬야한다.
소켓에 포트를 바인딩 하는 방법은 함수 bind()를 사용하면 된다.
int bind(SOCKET sock, const sockaddr* address, int address_len)
매개변수 sock은 바인딩할 소켓으로, socket()함수를 통해 만든다.
address는 이 소켓으로 보낼 패킷의 발신지 주소(회신 주소)이다.
멀티플레이어 게임용도로는 어느 네트워크 인터페이스에서 보냈는지 중요하지 않고,
따라서 호스트에 장착된 모든 네트워크 인터페이스의 ip주소의
해당 포트에 모두 바인딩하는게 바람직하다.
바인딩할 주소 sockaddr_in의 sin_addr 필드에 INADDR_ANY매크로 값을 넣으면 된다.
address_len에는 sockaddr의 길이를 넣어주어야 한다.
소켓에 sockaddr을 바인딩하면 운영체제가 이 주소와 포트를 목적지로 발신된 패킷을
수신하면 해당 소켓에 넣어준다.
또한 bind()에서 지정한 주소 및 포트를 이 소켓을 통해 나가는 패킷의
네트워크 계층과 전송 계층 헤더의 발신 주소와 포트로 사용된다.
하지만 주소와 포트를 확실히 고정해둘 필요가 없다면 네트워크 라이브러리는 자동으로
남아있는 포트에 소켓을 바인딩해주므로 사용을 안해도 된다.
SocketAddress가 IPv6도 지원받으려면 생성자를 하나 더 작성해야한다.
SocketAddress(uint8_t* inAddress, uint16_t inPort) {
GetAsSockAddrIn6()->sin6_family = AF_INET6;
GetAsSockAddrIn6()->sin6_port = htonl(inPort);
memcpy(GetAsSockAddrIn6()->sin6_addr.u.Byte, inAddress,sizeof(inAddress));
}
이런식으로 작성하였다.
SocketAddressFactory가 IPv6를 지원 받으려면
class SocketAddressFactory {
public:
static SocketAddressPtr CreateIPv4FromString(const string& inString) {
auto pos = inString.find_last_of(':');
string host, service;
//호스트 서비스가 정해졌다면 잘라서 넣어줌
if (pos != string::npos) {
host = inString.substr(0, pos);
service = inString.substr(pos + 1);
}
//없다면 포트에 디폴트값 넣어줌
else {
host = inString;
service = "0";
}
addrinfo hint;
memset(&hint, 0, sizeof(hint));
hint.ai_family = AF_INET6;
addrinfo* result = nullptr;
int error = getaddrinfo(host.c_str(), service.c_str(), &hint, &result);
//메모리해제를 위해 처음값 저장
addrinfo* initResult = result;
//getaddrinfo는 성공하면 0을 반환 따라서 addrinfo가져오기를 실패했다면
if (error != 0 && result != nullptr) {
//메모리 헤제 시키고
freeaddrinfo(initResult);
//nullptr리턴
return nullptr;
}
//result의 어드레스가 0이면서, result의 다음 addrinfo값이 있다면
while (!result->ai_addr && result->ai_next) {
//어드레스 가진 result나올때까지 계속 다음값으로
result = result->ai_next;
}
//만약 result가 끝값이라면 주소를 찾지 못한것이므로
if (!result->ai_next) {
//메모리 해제후 nullptr리턴
freeaddrinfo(initResult);
return nullptr;
}
auto toRet = std::make_shared<SocketAddress>(*result->ai_addr);
//
freeaddrinfo(initResult);
return toRet;
}
};
이런식으로 hit.ai_family를 AF_INET6로 설정해주면 힌트를 통해 addrinfo구조체를
IPv6형태로 바꿔준다.
SocketUtils클래스가 Tcp소켓의 생성해주는 스태틱 멤버함수를 구현해보자가 무슨 소리인지 잘 모르겠다.
기본적으로 서버에선 listen함수로 클라이언트 소켓을 받고,
accept함수로 클라이언트 소통용 소켓을 생성해 해당 소켓으로 통신하도록 구현하였다.
#include<iostream>
#include<winsock2.h>
#include<thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
# define Packet_size 1024
//리스닝 소켓, accept로 만들 소켓
SOCKET skt, client_skt;
int main(){
WSADATA wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
//socket()으로 리스닝 소켓을 만든 후,
skt= socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
sockaddr_in addr={};
addr.sin_family=AF_INET;
addr.sin_port=htons(4444);
addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
//bind()로 바인딩을 해준 후
bind(skt,(SOCKADDR*)&addr,sizeof(addr));
//listen함수를 이용해 리스닝을 시작
listen(skt,SOMAXCONN);
SOCKADDR_IN client={};
int clientSize=sizeof(client);
ZeroMemory(&client,sizeof(client));
//그 후 , TCP핸드셰이킹을 해나가기 위해 리스닝 소켓 skt를 이용해 accept함수를 호출하고
//성공적으로 accept함수가 실행된다면 accept에서 반환한 소켓은(client_sock) 클라이언트와 계속 통신하는 용도로 사용이 가능하다.
client_skt=accept(skt, (SOCKADDR*)&client,&clientSize);
char recvBuf[Packet_size]={};
while(!WSAGetLastError()){
ZeroMemory(recvBuf,sizeof(recvBuf));
recv(client_skt,recvBuf,Packet_size,0);
send(client_skt,recvBuf,sizeof(recvBuf),0);
}
closesocket(client_skt);
closesocket(skt);
WSACleanup();
}
//for using inet_addr
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<winsock2.h>
#include<thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
#define PACKET_SIZE 1024
SOCKET skt;
int main(){
WSADATA wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
skt=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
SOCKADDR_IN addr={};
addr.sin_family=AF_INET;
addr.sin_port=htons(4444);
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
while(1){
//서버는 connect()만 호출하면 된다.
if(!connect(skt,(SOCKADDR*)&addr,sizeof(addr))) break;
}
char msg[PACKET_SIZE]={0};
char recvBuf[PACKET_SIZE]={};
string cmd;
while(!WSAGetLastError()){
cin>>msg;
send(skt,msg,strlen(msg),0);
ZeroMemory(recvBuf,sizeof(recvBuf));
recv(skt,recvBuf,PACKET_SIZE,0);
cmd=recvBuf;
cout<<"My Message : "<<recvBuf<<endl;
}
closesocket(skt);
WSACleanup();
}
서버에 select함수를 적용한 코드
#include<iostream>
#include<winsock2.h>
#include<thread>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
# define Packet_size 1024
SOCKET server_skt, client_skt;
fd_set reads,copy_reads;
SOCKADDR_IN client={};
int clientSize=sizeof(client);
char recvBuf[Packet_size]={};
int main(){
WSADATA wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
//socket()으로 리스닝 소켓을 만든 후,
server_skt= socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
sockaddr_in addr={};
addr.sin_family=AF_INET;
addr.sin_port=htons(4444);
addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
//bind()로 바인딩을 해준 후
bind(server_skt,(SOCKADDR*)&addr,sizeof(addr));
//listen함수를 이용해 리스닝을 시작
listen(server_skt,SOMAXCONN);
//fd_set reads초기화
FD_ZERO(&reads);
//서버소켓 reads셋에 넣어줌
FD_SET(server_skt, &reads);
cout << "client Waiting" << '\n';
while (1)
{
// 원본 FD_SET의 보존을 위해 복사본을 생성하여 진행
copy_reads = reads;
// select함수 실행
int fd_num = select(0, ©_reads, NULL, NULL, NULL);
// select함수가 fd의 변화를 캐치한 후 , 복사본의 fd 갯수만큼 반복
for (int i = 0; i <= copy_reads.fd_count; i++)
{
// 복사본 set의 첫번째 소켓 읽어옴
SOCKET curSok = copy_reads.fd_array[i];
//복사본 set에서 curSok 찾았다면
if (FD_ISSET(curSok, ©_reads))
{
//server_skt라면 새로운 클라이언트가 접속했다는 뜻 (listen함수)
if (curSok == server_skt)
{
cout << "new Client" << '\n';
//SOCKADDR_IN구조체 초기화 해준 후,
ZeroMemory(&client, sizeof(client));
//accept함수로 새로운 클라이언트 소켓 할당
client_skt = accept(server_skt, (sockaddr *)&client, &clientSize);
//클라이언트 소켓 reads에 넣어줘서 관리
FD_SET(client_skt, &reads);
}
//이미 관리하고 있는 클라이언트에서 메세지가 온것이라면
else
{
//클라이언트에서 온 메세지 저장할 recvBuf 초기화
ZeroMemory(recvBuf, sizeof(recvBuf));
//recv함수 실행 후 반환 값 read_num에 저장
int read_num = recv(curSok, recvBuf, Packet_size, 0);
//만약 반환 값이 0 이하라면 오류가 생겼으므로 해당 클라이언트 제거 처리
if (read_num < 0)
{
cout << curSok << " Client Quit" << '\n';
//해당 소켓 close한 후
closesocket(curSok);
//reads fdset에서 해당 소켓 제거
FD_CLR(curSok, &reads);
}
//제대로된 메시지를 수신했을 때
else
{
cout << recvBuf << " Message from Client" << '\n';
//echo~
send(curSok, recvBuf, read_num, 0);
}
}
//변화 발생한 파일 디스크립터 수가 1개 이하면 break해서 반복문 탈출하게함
if (--fd_num <= 0)
break;
}
}
}
closesocket(server_skt);
WSACleanup();
}
클라이언트에 논블로킹방식 ioctlsocket을 적용한 코드
#include<iostream>
#include<winsock2.h>
#include<thread>
using namespace std;
#define PACKET_SIZE 1024
SOCKET skt;
int main(){
WSADATA wsa;
WSAStartup(MAKEWORD(2,2),&wsa);
skt=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
SOCKADDR_IN addr={};
addr.sin_family=AF_INET;
addr.sin_port=htons(4444);
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
//0 for non-blocking, 1 for blocking
ioctlsocket(skt,FIONBIO,0);
//서버는 connect()만 호출하면 된다.
while(true){
if (connect(skt, (SOCKADDR *)&addr, sizeof(addr)) == SOCKET_ERROR)
{
//만약 에러가 WSAEWOULDBLOCK이라면 논블로킹으로 설정해서 나오는것이므로 컨티뉴
if (WSAGetLastError() == WSAEWOULDBLOCK)
continue;
//아니라면 error
break;
}
}
cout<<"Server Connected"<<'\n';
char msg[PACKET_SIZE]={0};
char recvBuf[PACKET_SIZE]={};
string cmd;
while(1)
{
//보낼 메세지
cin >> msg;
if(send(skt, msg, strlen(msg), 0)==SOCKET_ERROR){
//에러가 WSAEWOULDBLOCK이라면 논블로킹이라 뜨는 메시지로 컨티뉴
if(WSAGetLastError()==WSAEWOULDBLOCK)
continue;
break;
}
//수신 버퍼 초기화
ZeroMemory(recvBuf, sizeof(recvBuf));
while(true){
//recv함수의 반환값 errCode에 저장
int errCode=recv(skt, recvBuf, PACKET_SIZE, 0);
//에러 났을 때
if(errCode==SOCKET_ERROR){
//WSAEWOULDBLOCK이라면 논블로킹이라 에러 뜨는 것이므로 continue;
if(WSAGetLastError()==WSAEWOULDBLOCK)
continue;
break;
}
//WSAEWOULDBLOCK가 아니라면 에러
else if(errCode==0){
break;
}
//메세지출력
cout<<"My message from Server : "<<recvBuf<<'\n';
break;
}
}
closesocket(skt);
WSACleanup();
}
https://1d1cblog.tistory.com/356
https://dodo000.tistory.com/31
https://yms2047.tistory.com/entry/select-%ED%95%A8%EC%88%98-%EC%82%AC%EC%9A%A9%EB%B2%95
https://sanggoe.github.io/study/2020/12/23/network-Chap4_%EA%B3%A0%EA%B8%89_%EC%86%8C%EC%BC%93_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D.html
https://dockdocklife.tistory.com/entry/%EB%85%BC%EB%B8%94%EB%A1%9D%ED%82%B9-%EC%86%8C%EC%BC%93