짧게 설명드리자면

클라이언트는 웹브라우저가 해줄거라 따로 필요없고

서버는 소켓생성 -> bind -> listen -> accept의 과정을 거치며

accepting loop에서 클라이언트의 요청이 들어오면 그에 맞게 response를 보냅니다.

 

몇년 전에 학교 과제용으로 만든거라 그냥 참고용으로만 봐주세요,,

우분투(64bit)에서 만들고 실행했었습니다.

따로 프로세스나 쓰레드를 생성하진 않았어요.

각 부분별로 설명을 드리자면..

헤더파일 선언 및 listen함수에 들어갈 BACKLOG값, 버퍼사이즈, 파일 이름 크기 정의입니다.

 

클라이언트가 파일을 요청하면 서버는 파일의 내용을 읽어와 보내주는데, 웹 브라우저가 이를 띄우기 위해선 앞에 html형식의 헤더를 붙여야합니다. 헤더가 없으면 제대로 사진 등을 출력하지 않아요.

어차피 앞부분은 다 똑같으니 image/jpeg , image/gif, audio/mp3, application/pdf, text/html만 따로 구분해서 만들고

앞부분도 따로 하면 메모리를 조금 덜 먹을 수 있겠습니다...

defaulthtml은 서버에 없는 파일을 요청했을 경우 보내는 html 메세지입니다.

 

실행파일 이름이 server인 경우 $./server [PORT번호] 로 실행하게 되는데, 포트번호를 입력하지 않은 경우 프로그램을 종료합니다.

소켓 파일디스크립터를 만듭니다. 

서버의 IP, 포트번호를 설정하고 

그를 이용해 소켓을 bind해줍니다.

 

listen함수입니다. BACKLOG값은 대기중인 큐에 넣을 최대 연결 수 라고 하는데 자세한건 더 찾아보시면 좋을것 같네요

 

이제 아래는 accept loop입니다.

클라이언트의 주소를 받고

콘솔창에 클라이언트의 주소를 띄워줍니다.

클라이언트가 보낸 값을 읽어옵니다.

클라이언트가 보낸 값중 GET / 뒷부분의 위치를 받고(5는 "GET /"의 length입니다)

띄어쓰기 전까지의 파일이름을 받고 뒷부분을 \0으로 채웁니다.

현재 작업중인 디렉토리에서 파일을 찾고 없으면 404 not found 메세지를 보냅니다.(defaulthtml)

 

파일이 있으면 확장자별로 헤더를 덧붙이고 전송합니다.

 

지금보니까 코딩 겁나 드럽게했네요..

암튼 서버 실행화면은 이렇게 생겼구요

포트번호 9999번으로 했을 경우

http://127.0.0.1:9999/testgif.gif 를 주소창에 치면

대충 요런식으로 파일을 읽어옵니다.

mp3,pdf,jpeg 등 파일을 열 수 있습니다.

 

봐주셔서 감사합니다.

다시 말씀드리지만 하도 오래된 코드라 참고용으로만 봐주셨으면 좋겠습니다..

아래는 복사가능한 전체코드입니다.

더보기

#include <stdio.h>
#include <arpa/inet.h> // GET IP ADDRESS IN 64 BIT MACHINE
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#define BACKLOG 10
#define BUF_SIZE 1024
#define FILE_NAME 100
//FILE HEADER(before sending file, server send this header to client)
char imageheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: image/jpeg\r\n\r\n";
char gifheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: image/gif\r\n\r\n";
char mp3header[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: audio/mp3\r\n\r\n";
char pdfheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: application/pdf\r\n\r\n";
char htmlheader[] = "HTTP/1.1 200 Ok\r\n" "Content-Type: text/html\r\n\r\n";
char defaulthtml[] =  "HTTP/1.1 404 Not Found\r\n"
"Content-Type : text/html; charset=UTF-8\r\n\r\n"
"<!DOCTYPE html>\r\n"
"<html><head><title>WELCOM TO MY SERVER</title>\r\n"
"<body>I've got wrong file name. check it please.</body>"
"</html>";
int main(int argc, char *argv[])
{
int sockfd, new_fd; // sockfd : server socket fd, new_fd : client socket fd
struct sockaddr_in my_addr; // server addr
struct sockaddr_in their_addr; // client addr
char buffer[BUF_SIZE];
char *strptr1;
int count=0;
int n; // reading variable
int sin_size; // client addr size
int filesize;
int PORT = atoi(argv[1]);
if(argc<2){
printf("error (no port)\n");
exit(0);
} // if there is no port #, terminate program.

///////////////////MAKE SOCKET////////////////////
if((sockfd=socket(PF_INET, SOCK_STREAM, 0))==-1)
{
perror("socket error");
exit(0);
}
///////////////////BIND SOCKET////////////////////
memset(&my_addr,0,sizeof(my_addr)); // clear by 0
my_addr.sin_family = AF_INET; // AF_INET in IPv4
my_addr.sin_port = htons(PORT); // port # , short type data(PORT)
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 32bit IPv4 addr , long type data(IP)
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))==-1)
{
perror("bind error");
exit(0);
}
//////////////////////LISTEN//////////////////////
if(listen(sockfd, BACKLOG)==-1)
{
perror("listen error");
exit(0);
}
//////////////////////////////////////////////////
sin_size = sizeof(struct sockaddr_in);
////////////////////ACCEPTING/////////////////////

while(1){ // accepting loop
if((new_fd=accept(sockfd, (struct sockaddr*)&their_addr, &sin_size))==-1)
{
perror("fail to accepting");
continue;
}
//show client's ip address
printf("server : got connection from %s\n", inet_ntoa(their_addr.sin_addr));
//read request message and print it
bzero(buffer,BUF_SIZE);
n=read(new_fd,buffer,BUF_SIZE);
if(n<0) perror("READING ERROR");
printf("%s", buffer);
///////FIND FILENAME///////
strptr1 = strstr(buffer,"GET /");
strptr1 += 5;
char filename[100];
char pwd[BUF_SIZE];
memset(filename,0,100);
int i=0;
int fd;
while(1)
{
if(strptr1[i]==' ')
{
filename[i] = '\0'; break;
}
filename[i]=strptr1[i];
i++;
}//get filename until meet the ' ' character.
/////FIND FILE IN WORKING DIRECTORY/////
getcwd(pwd,BUF_SIZE);
strcat(pwd,"/");
strcat(pwd,filename);
char fbuf[BUF_SIZE];
/////FILE is not in working directory/////
if((fd=open(pwd,O_RDONLY))==-1)
{
        write(new_fd,defaulthtml,sizeof(defaulthtml)-1);
printf("Server sent '404 not found message' to client\n");
}
/////FILE is in working directory/////
////////SENDING FILE TO CLIENT////////
else
{ //SEND FILE HEADER//
if(strstr(filename,".jpg")!=NULL||strstr(filename,".jpeg")!=NULL)
write(new_fd,imageheader,sizeof(imageheader)-1);
else if(strstr(filename,".gif")!=NULL)
write(new_fd,gifheader,sizeof(gifheader)-1);
else if(strstr(filename,".pdf")!=NULL)
write(new_fd,pdfheader,sizeof(pdfheader)-1);
else if(strstr(filename,".mp3")!=NULL)
write(new_fd,mp3header,sizeof(mp3header)-1);
else if(strstr(filename,".html")!=NULL)
write(new_fd,htmlheader,sizeof(htmlheader)-1);
        //SEND FILE//
while((n=read(fd,fbuf,BUF_SIZE))>0)
         write(new_fd,fbuf,n);
        printf("Server sent file completly : %s\n\n", filename);
}
close(new_fd);
close(fd);
}
}

화면 구성은 아래와 같습니다.

 

화면의 각 구성요소

1. 서버 연결상태 출력 (UI-Text)

2. 유저 아이디 입력 (UI-Input Field)

3. 아이디 입력 후 사용할 접속 버튼 (UI-Button)

계층

위에서 설명한 ID(Input Field), Login(Button), ConnectionStatus(Text)

그리고 로그인 화면 작업을 담당할 LobbyManager(Empty) 입니다.

 

LobbyManager 코드

LobbyManager.cs 스크립트는 다음과 같습니다.

C# 스크립트 생성 후 LobbyManager.cs 로 만들고, Empty object인 Lobby Manager에 추가하고, 다음과 같이 드래그앤 드롭 합니다.

Login Btn은 접속 버튼, IDtext는 Input Field의 Text, Connection Status는 접속 상태 출력 Text입니다.

마찬가지로, 접속 버튼이 눌러졌을때 실행할 메소드를 LobbyManager의 Connect()로 설정합니다.

소스코드를 각 부분별로 설명하자면,

LobbyManager는 MonoBehaviorPunCallbacks 를 상속받습니다.

Start 메소드는 시작과 동시에 PhotonNetwork.ConnectUsingSettings(); 메소드를 이용해 Photon 서버와 연결하며,

연결하는 동안 loginBtn.interactoable = false; 로 접속 버튼을 누르지 못하게 하고 연결상태를 출력합니다.

Connect 메소드는 접속 버튼을 누르면 실행할 메소드로, ID를 입력하지 않았다면 아무 반응을 하지 않습니다. ID를 입력해달라는 메세지를 띄우셔도 좋습니다. ID를 입력했다면 PhotonNetwork.LocalPlayer.NickName = IDtext.text; 코드를 통해 해당 로컬플레이어의 닉네임(ID)을 입력한 텍스트로 저장합니다.

그 후 Start 메소드에서 성공적으로 Photon 서버와 연결되었다면 PhotonNetwork.JoinRandomRoom() 메소드를 통해 채팅할 방에 입장하며, Start 메소드에서 Photon 서버와 연결이 되지 않았다면 재접속을 시도합니다.

OnConnectedToMaster 메소드는 Photon 서버(Master서버)와 연결되면 실행되는 메소드로, 접속 버튼을 사용 가능케 하며 연결 상태를 온라인으로 바꿉니다.

OnDisconnected 메소드는 Photon 서버와 연결이 끊겼을 경우 접속 버튼을 다시 사용 불가능하게 하며 연결 상태를 오프라인으로 바꿔 출력합니다. 그 후 서버와 재접속을 시도합니다.

OnJoinRandomFailed 메소드는 Connect 메소드에서 JoinRandomRoom 메소드를 실행했으나 방에 들어가지 못한 경우 실행되는 메소드로, 적합한 방이 없으면 PhotonNetwork.CreateRoom을 이용해 새 방을 만듭니다. 여기서 CreateRoom의 첫번째 인자는 방의 이름, 두번째 인자는 방의 옵션입니다. 여기선 방의 최대 인원을 4로 설정했습니다. (참고로 MaxPlayers = 0으로 하면 인원 제한을 없앨 수 있습니다.)

OnJoinedRoom 메소드는 방에 성공적으로 입장했을 경우 실행되는 메소드로, 씬을 Main(채팅을 진행할 씬)으로 옮깁니다. SceneManager.LoadScene()과 달리 PhotonNetwork.LoadLevel() 메소드를 사용한 이유는 네트워크의 정보를 그대로 가져가기 위함입니다.

 

완성된 로그인 씬입니다. ID를 입력하지 않고 접속버튼을 누르면 반응이 없으며, ID 입력후 접속에 성공하면 Main 씬으로 갑니다.

 

더보기 버튼을 누르시면 복사 가능한 전체 코드를 보실 수 있습니다

더보기

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;

public class LobbyManager : MonoBehaviourPunCallbacks
{
    public Button loginBtn;
    public Text IDtext;
    public Text ConnectionStatus;
    // Start is called before the first frame update
    void Start()
    {
        PhotonNetwork.ConnectUsingSettings();
        loginBtn.interactable = false;
        ConnectionStatus.text = "Connecting to Master Server...";
    }


    public void Connect()
    {
        if (IDtext.text.Equals(""))
        {
            return;
        }
        else
        {
            PhotonNetwork.LocalPlayer.NickName = IDtext.text;
            loginBtn.interactable = false;
            if (PhotonNetwork.IsConnected)
            {
                ConnectionStatus.text = "connecting to room...";
                PhotonNetwork.JoinRandomRoom();
            }
            else
            {
                ConnectionStatus.text = "Offline : failed to connect.\nreconnecting...";
                PhotonNetwork.ConnectUsingSettings();
            }

        }

    }
    public override void OnConnectedToMaster()
    {
        loginBtn.interactable = true;
        ConnectionStatus.text = "Online : connected to master server";
    }
    public override void OnDisconnected(DisconnectCause cause)
    {
        loginBtn.interactable = false;
        ConnectionStatus.text = "Offline : failed to connect.\nreconnecting...";
        PhotonNetwork.ConnectUsingSettings();
    }
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        ConnectionStatus.text = "No empty room. creating new room...";
        PhotonNetwork.CreateRoom(null, new RoomOptions { MaxPlayers = 4 });
    }
    public override void OnJoinedRoom()
    {
        ConnectionStatus.text = "Success to join room";
        PhotonNetwork.LoadLevel("Main");
    }
}

'C# > 유니티' 카테고리의 다른 글

Photon 채팅프로그램 - 2. 채팅 씬  (0) 2020.03.26
Photon 채팅 프로그램 - 0. 준비  (0) 2020.03.26

+ Recent posts