728x90

React로 프론트엔드를 설계하고, Express.js서버로 간단한 CRUD 기능이 있는 커뮤니티를 제작하고 있다.

 

client와 server의 폴더를 완전히 분리시켜서 작업하였고, concurrently와 nodemon을 이용하여 개발을 하였다.

 

Backend의 Architecture는 이렇다.

MYUN_SJ backend/  
├── app.js  
├── package.json  
├── /config  
│   └── db.js  
├── /models  
│   ├── user.js  
│   └── post.js  
├── /routes  
│   ├── index.js  
│   ├── users.js  
│   └── posts.js  
├── /controllers  
│   ├── usersController.js  
│   └── postsController.js  
├── /middlewares  
│   └── authMiddleware.js  
├── /public  
│   ├── /images  
│   ├── /javascripts  
│   └── /stylesheets  
└── /views (RESTful API이기 때문에 제거)

 

지금은 개발단계이기 때문에 localhost로 진행하였고, public이나 index.js의 활용은 아직 하고있지 않는다.

 

완전히 분리하여 포트번호도 다르기 때문에, cors정책을 위반하게 된다. 이를 처리하는 로직을 express 서버의 main 호출단계에 추가해주었다. main단에서는 최대한 호출정도의 기능만 하기 위하여 최대한 압축시켰다.

const express = require('express');
const app = express();
const cors = require('cors');
const session = require('express-session');
require('dotenv').config();

const sequelize = require('./config/db');

const User = require('./models/user');
const Post = require('./models/post');

const postsRoutes = require('./routes/posts');
const usersRoutes = require('./routes/users');

app.use(cors());

app.use((req, res, next) => {
    res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.header('Pragma', 'no-cache');
    res.header('Expires', '0');
    next();
});

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(session({
    secret: process.env.SESSION_KEY,
    resave: false,
    saveUninitialized: true,
    cookie: { secure: 'auto' }
}));

app.use('/api/posts', postsRoutes);
app.use('/api/users', usersRoutes);

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
    sequelize.authenticate().then(() => {
        console.log('Database connected...');
        sequelize.sync().then(() => {
            console.log('Tables created...');
        }).catch(err => console.log('Error: ' + err));
    }).catch(err => console.log('Error: ' + err));
});

 

DB는 MySQL의 sequelize를 사용하여 편하게 설계하였다. GMT 시간을 맞추는 로직이 필요하였다.

require('dotenv').config();
const { Sequelize } = require('sequelize');

const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
    host: process.env.DB_HOST,
    dialect: 'mysql',
    timezone: '+09:00'
});

sequelize.authenticate()
    .then(() => console.log('Connection has been established successfully.'))
    .catch(err => console.error('Unable to connect to the database:', err));

module.exports = sequelize;

 

프론트에서 최대한 요청을 하고, 백에서 최대한 처리를 하기 위하여 로그아웃 로직도 백에 추가시켜주었다. Node.js이기 때문에 적합한 Rest.API를 채택하여 통신을 하였다.

const User = require('../models/user');

const usersController = {
    // 모든 사용자 조회
    getAllUsers: async (req, res) => {
        try {
            const users = await User.findAll();
            res.json(users);
        } catch (error) {
            res.status(500).json({ message: error.message });
        }
    },

    // 사용자 생성
    createUser: async (req, res) => {
        try {
            const user = await User.create(req.body);
            res.status(201).json(user);
        } catch (error) {
            res.status(400).json({ message: error.message });
        }
    },

    // 특정 사용자 조회
    getUserById: async (req, res) => {
        try {
            const user = await User.findByPk(req.params.id);
            if (user) {
                res.json(user);
            } else {
                res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
            }
        } catch (error) {
            res.status(500).json({ message: error.message });
        }
    },

    // 사용자 업데이트
    updateUser: async (req, res) => {
        try {
            const user = await User.findByPk(req.params.id);
            if (user) {
                await user.update(req.body);
                res.json(user);
            } else {
                res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
            }
        } catch (error) {
            res.status(400).json({ message: error.message });
        }
    },

    // 사용자 삭제
    deleteUser: async (req, res) => {
        try {
            const user = await User.findByPk(req.params.id);
            if (user) {
                await user.destroy();
                res.status(204).send();
            } else {
                res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
            }
        } catch (error) {
            res.status(500).json({ message: error.message });
        }
    },

    // 사용자 로그인
    loginUser: async (req, res) => {
        const { email, pw } = req.body;
        try {
            const user = await User.findOne({ where: { email: email } });
            if (!user) {
                return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
            }

            // 비밀번호 검증 로직 추가 일치 여부만 확인
            if (user.pw !== pw) {
                return res.status(400).json({ message: '잘못된 비밀번호입니다.' });
            }

            // 세션에 사용자 ID 저장
            req.session.userId = user.id;
            res.json({ message: '로그인 성공!' });
        } catch (error) {
            res.status(500).json({ message: error.message });
        }
    },

    // 사용자 로그아웃
    logoutUser: async (req, res) => {
        try {
            req.session.destroy((err) => {
                if (err) {
                    console.log(err);
                    res.status(500).json({ message: '로그아웃 에러' });
                } else {
                    res.json({ message: '로그아웃 성공' });
                }
            });
        } catch (error) {
            res.status(500).json({ message: error.message });
        }
    }
};

module.exports = usersController;

 

세션 방식을 이용하여 통신했기 때문에 bcrypt 등(아직 추가하지 않음) 여러 검증 로직이 필요했었다.

function authMiddleware(req, res, next) {
    if (req.session && req.session.userId) {
        next(); // 세션 정보가 유효하면 다음 미들웨어로 진행
    } else {
        res.status(401).send('Unauthorized'); // 로그인되지 않은 사용자에게 401 상태 코드 응답
    }
}

module.exports = authMiddleware;

 

https://github.com/MYun-SJ/MYun_SJ/tree/main/server

 

MYun_SJ/server at main · MYun-SJ/MYun_SJ

MYun_SJ. Contribute to MYun-SJ/MYun_SJ development by creating an account on GitHub.

github.com

 

728x90

'서버' 카테고리의 다른 글

Proxy 서버 구현  (1) 2024.03.24
echo server, tiny server 구현  (2) 2024.03.24
Socket, Redirection, Pipe  (1) 2024.03.23
Parsing, Caching, Filtering, Load Balancing, MTU, NAT  (1) 2024.03.23
728x90

프록시 서버는 인터넷 사용 시 '중계자'로서 중요한 역할을 한다. 이를 통해 온라인 활동은 보다 안전하고 효율적으로 이루어진다. 프록시 서버에는 주로 두 가지 종류가 있다.

 

 

포워드 프록시는 사용자의 온라인 신원을 숨기는 역할을 한다. 인터넷에 접속할 때 직접 서버로 연결하지 않고 프록시 서버를 거쳐서, 사용자의 IP 주소와 같은 개인 정보의 노출을 막는다. 이는 마치 온라인에서 가면을 쓰고 다니는 것과 같아 사용자를 보호한다.

 

 

리버스 프록시는 서버 측에서 사용되며, 외부의 요청을 받아 내부 서버로 전달하는 역할을 한다. 이를 통해 보안을 강화하고, 여러 서버가 요청을 효율적으로 처리할 수 있도록 돕는다. 실제 서버들은 내부망에 숨어 있으며, 외부에서는 리버스 프록시를 통해서만 접근할 수 있다.

 

프록시 체이닝은 여러 프록시 서버를 연속적으로 사용하는 기술로, 사용자의 IP 주소를 더욱 효과적으로 숨길 수 있다. 해외 프록시를 포함하여 여러 단계의 프록시를 거치면 추적이 더 어려워진다.

 

프록시 서버는 다음과 같은 주요 기능을 제공한다.

  • 방화벽: 외부의 위협으로부터 내부 네트워크를 보호한다.
  • 익명화: 사용자의 신원을 숨기고 익명으로 인터넷을 사용할 수 있도록 한다. 프록시는 요청에서 식별 정보를 제거하여 이를 가능하게 한다.
  • 캐시: 자주 접속하는 웹 사이트의 데이터를 저장해 놓고, 다음 접속 시 빠르게 로드할 수 있도록 도와준다.

proxy.c(main)

int main(int argc,char **argv)
{
    int listenfd,connfd;
    socklen_t  clientlen;
    char hostname[MAXLINE],port[MAXLINE];
    struct sockaddr_storage clientaddr;
    if(argc != 2){
        fprintf(stderr,"usage :%s <port> \n",argv[0]);
        exit(1);
    }
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);
        Getnameinfo((SA*)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
        printf("Accepted connection from (%s %s).\n",hostname,port);
        doit(connfd);
        Close(connfd);
    }
    return 0;
}

 

1. Open_listenfd 함수를 호출해 주어진 포트 번호에 대한 리슨 소켓을 열고, 클라이언트의 연결 요청을 기다릴 준비를 한다.

 

2. 프로그램은 무한히 실행되며, while(1) 루프 안에서 Accept 함수를 사용하여 클라이언트의 연결 요청을 기다리고 수락한다. 연결 요청이 들어오면, 클라이언트와 통신하기 위한 새로운 소켓(connfd)을 얻는다.

 

3. 연결된 클라이언트의 IP 주소와 포트 번호를 얻기 위해 Getnameinfo 함수를 사용한다. 이 정보는 연결이 수락되었음을 나타내는 메시지에 사용된다.

 

4. doit 함수를 호출하여 클라이언트로부터 받은 요청을 처리한다.

 

5. 클라이언트의 요청을 처리한 후, Close 함수를 사용하여 클라이언트와의 연결을 닫는다. 이는 서버가 사용한 리소스를 해제하고 다음 연결 요청을 준비하는 단계이다.

 

proxy.c(doit)

void doit(int connfd){
  int end_serverfd;
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char endserver_http_header[MAXLINE];
  char hostname[MAXLINE], path[MAXLINE];
  int port;
  rio_t rio,rio_endserver;
  Rio_readinitb( & rio, connfd);
  Rio_readlineb( & rio, buf, MAXLINE);
  printf("Request headers:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version);
  if (strcasecmp(method, "GET")) {
    printf("Error code: 501\n");
    return;
  }
  parse_uri(uri,hostname,path,&port);
  build_http_header(endserver_http_header,hostname,path,port,&rio);
  end_serverfd = connect_endServer(hostname,port,endserver_http_header);
  if(end_serverfd<0){
    printf("connection failed\n");
    return;
  }
  Rio_readinitb(&rio_endserver,end_serverfd);
  Rio_writen(end_serverfd,endserver_http_header,strlen(endserver_http_header));
  size_t n;
  while((n=Rio_readlineb(&rio_endserver,buf,MAXLINE))!=0){
    printf("Proxy received %d bytes,then send\n",n);
    Rio_writen(connfd,buf,n);
  }
  Close(end_serverfd);
}

 

1. 필요한 변수를 초기화하고, 클라이언트 연결 파일 디스크립터(connfd)를 사용하여 클라이언트로부터 HTTP 요청 헤더를 읽는다.

 

2. 요청에서 HTTP 메소드(GET 등)를 추출하고, 이 메소드가 GET이 아닐 경우 오류 코드 501(Not Implemented)를 출력하고 함수에서 빠져나온다.

 

3. 요청된 URI에서 호스트 이름, 경로(path), 포트를 추출한다. 이 정보는 엔드 서버로의 연결 설정에 필요하다.

 

4. 엔드 서버로 전송할 새로운 HTTP 헤더를 구성한다. 이 과정에서 호스트 이름, 경로, 포트 정보 등을 사용하여 헤더를 작성한다.

 

5. 추출된 호스트 이름과 포트를 사용하여 엔드 서버에 연결을 시도하고, 연결에 성공하면 엔드 서버와의 통신을 위한 새로운 파일 디스크립터(end_serverfd)를 얻는다. 연결 실패 시 오류 메시지를 출력하고 함수에서 빠져나온다.

 

6. 구성한 HTTP 헤더를 엔드 서버로 전송하고 엔드 서버로부터 응답을 받아 클라이언트에게 전달한다. 이 과정에서 프록시 서버는 엔드 서버로부터 받은 데이터를 그대로 클라이언트에게 전송한다.

 

7. 엔드 서버와의 연결을 종료한다.

 

proxy.c(build_http_header)

void build_http_header(char *http_header,char *hostname,char *path,int port,rio_t *client_rio){
  char buf[MAXLINE],request_hdr[MAXLINE],other_hdr[MAXLINE],host_hdr[MAXLINE];
  sprintf(request_hdr,requestlint_hdr_format,path);
  while(Rio_readlineb(client_rio,buf,MAXLINE)>0){
    if(strcmp(buf,endof_hdr)==0) break;
    if(!strncasecmp(buf,host_key,strlen(host_key))){
      strcpy(host_hdr,buf);
      continue;
    }
    if(!strncasecmp(buf,connection_key,strlen(connection_key))&&
    !strncasecmp(buf,proxy_connection_key,strlen(proxy_connection_key))&&
    !strncasecmp(buf,user_agent_key,strlen(user_agent_key))){
      strcat(other_hdr,buf);
    }
  }
  if(strlen(host_hdr)==0){
    sprintf(host_hdr,host_hdr_format,hostname);
  }
  sprintf(http_header,"%s%s%s%s%s%s%s",request_hdr,host_hdr,conn_hdr,prox_hdr,user_agent_hdr,other_hdr,endof_hdr);
  return;
}

 

1. 필요한 헤더 문자열을 저장할 버퍼들을 초기화한다.

 

2. 클라이언트로부터 받은 URI의 경로(path) 정보를 사용하여 HTTP 요청 라인을 생성한다. 예를 들어, GET /path HTTP/1.1과 같은 형태이다.

 

3. 함수를 사용하여 클라이언트로부터 한 줄씩 헤더를 읽는다. 헤더의 끝을 나타내는 빈 줄을 만날 때까지 이 작업을 반복한다.

 

4. 헤더를 처리하고 처리할 때 생성한 요청 라인, Host 헤더, 필수 헤더(Connection, Proxy-Connection, User-Agent), 그리고 다른 헤더들을 하나의 HTTP 요청 헤더로 조합한다.

 

5. 최종적으로 조합된 HTTP 요청 헤더를 http_header에 저장하여 함수를 종료한다.

 

위 코드는 해당 URL yunsejin/proxy 브랜치에 있다.

 

GitHub - yunsejin/Webproxy-lab

Contribute to yunsejin/Webproxy-lab development by creating an account on GitHub.

github.com

728x90

'서버' 카테고리의 다른 글

Express.js Clean Architecture 구성하기  (0) 2024.04.12
echo server, tiny server 구현  (2) 2024.03.24
Socket, Redirection, Pipe  (1) 2024.03.23
Parsing, Caching, Filtering, Load Balancing, MTU, NAT  (1) 2024.03.23
728x90

Echo server는 클라이언트로부터 받은 메시지를 그대로 돌려보내는 간단한 서버다. 네트워크 연결, 소켓 프로그래밍, 서버-클라이언트 아키텍처 등을 이해하기 위한 교육용 또는 테스트용으로 주로 사용된다.

 

echoserveri.c

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    char client_hostname[MAXLINE], client_port[MAXLINE];

    if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(0);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
    clientlen = sizeof(struct sockaddr_storage);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
                    client_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, client_port);
    echo(connfd);
    Close(connfd);
    }
    exit(0);
}

 

1. 프로그램은 실행할 때 하나의 인자(포트 번호)를 받아야 한다. 인자의 개수(argc)가 2가 아니면(프로그램 이름 포함), 사용법을 출력하고 프로그램을 종료한다.

 

2. Open_listenfd 함수는 제공된 포트 번호에 대해 듣기(listen) 상태의 소켓을 열고, 이 소켓의 파일 기술자를 반환한다. 이 함수는 socket(), bind(), listen() 시스템 호출을 캡슐화하여 사용하기 쉽게 만든 것이다.

 

3. 무한 루프 안에서, 서버는 Accept 함수를 통해 클라이언트의 연결 요청을 기다린다. 연결이 수립되면, 클라이언트의 주소 정보를 가져오기 위해 Getnameinfo 함수를 사용한다. 이 정보는 클라이언트의 호스트 이름과 포트 번호를 문자열로 변환한다.

 

4. 클라이언트로부터 받은 데이터를 처리하고, 그 데이터를 클라이언트에게 다시 전송하는 echo 함수를 호출한다. echo 함수의 구현은 제공되지 않았으나, 일반적으로 클라이언트로부터 데이터를 읽고(read), 동일한 데이터를 클라이언트로 다시 쓰는(write) 작업을 수행한다.

 

echoclient.c

#include "csapp.h"

int main(int argc, char **argv)
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

    if (argc != 3) {
    fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
    exit(0);
    }
    host = argv[1];
    port = argv[2];

    clientfd = Open_clientfd(host, port);
    Rio_readinitb(&rio, clientfd);

    while (Fgets(buf, MAXLINE, stdin) != NULL) {
    Rio_writen(clientfd, buf, strlen(buf));
    Rio_readlineb(&rio, buf, MAXLINE);
    Fputs(buf, stdout);
    }
    Close(clientfd); //line:netp:echoclient:close
    exit(0);
}

 

1. 프로그램은 실행할 때 두 개의 인자(호스트와 포트)를 받아야 한다. 인자의 개수(argc)가 3이 아니면 사용법을 출력하고 프로그램을 종료한다.

 

2. Open_clientfd 함수를 사용해 주어진 호스트와 포트에 대한 연결을 시도하고, 연결된 소켓의 파일 기술자를 반환한다. 이 함수는 socket(), connect() 등의 시스템 호출을 캡슐화한다.

 

3. Rio_readinitb 함수를 사용해 robust I/O (RIO) 패키지를 초기화한다. 이 패키지는 네트워크 I/O를 위한 더 안정적이고 편리한 인터페이스를 제공한다.

 

4. Fgets, Fputs를 이용한 루프는 EOF(End Of File)를 입력할 때까지 (예: 터미널에서 Ctrl+D, C를 누를 때) 계속된다.

echoserver 테스트

 

Tiny server는 극도로 가벼운 웹 서버를 의미한다. 리소스가 제한된 환경에서 작동하거나, 간단한 웹 기반 애플리케이션을 호스팅하는 데 사용될 수 있다. 가볍다는 특성 때문에 빠른 성능을 요구하는 작은 규모의 프로젝트나 임베디드 시스템에서 유용하다.

 

tiny.c(main)

int main(int argc, char **argv)
{
  int listenfd, connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;

  /* Check command line args */
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }

  listenfd = Open_listenfd(argv[1]);
  while (1)
  {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // line:netp:tiny:accept
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    doit(connfd);  // line:netp:tiny:doit
    Close(connfd); // line:netp:tiny:close
  }
}

 

프로그램은 정확히 하나의 명령줄 인자(서버가 리슨할 포트 번호)를 기대한다. 만약 제대로 된 인자가 제공되지 않으면, 사용법을 출력하고 프로그램을 종료한다.

 

Open_listenfd 함수를 호출하여 리슨 소켓을 생성하고, 제공된 포트 번호에 바인드한다. 이 함수는 내부적으로 socket(), bind(), listen() 시스템 호출을 수행한다.

 

tiny.c(parse_uri)

int parse_uri(char *uri, char *filename, char *cgiargs)
{
  char *ptr;

  if (!strstr(uri, "cgi-bin"))
  { /* Static content */             // line:netp:parseuri:isstatic
    strcpy(cgiargs, "");             // line:netp:parseuri:clearcgi
    strcpy(filename, ".");           // line:netp:parseuri:beginconvert1
    strcat(filename, uri);           // line:netp:parseuri:endconvert1
    if (uri[strlen(uri) - 1] == '/') // line:netp:parseuri:slashcheck
      strcat(filename, "adder.html"); // line:netp:parseuri:appenddefault
    return 1;
  }
  else
  { /* Dynamic content */  // line:netp:parseuri:isdynamic
    ptr = index(uri, '?'); // line:netp:parseuri:beginextract
    if (ptr)
    {
      strcpy(cgiargs, ptr + 1);
      *ptr = '\0';
    }
    else
      strcpy(cgiargs, ""); // line:netp:parseuri:endextract
    strcpy(filename, "."); // line:netp:parseuri:beginconvert2
    strcat(filename, uri); // line:netp:parseuri:endconvert2
    return 0;
  }
}

 

정적 콘텐츠 처리:

1. strstr 함수를 사용하여 URI에 "cgi-bin" 문자열이 포함되어 있는지 확인한다. 포함되어 있지 않다면, 요청은 정적 콘텐츠로 간주된다.

 

2. cgiargs를 빈 문자열로 설정한다. 이는 정적 콘텐츠 요청에서 CGI 인자가 필요 없음을 의미한다.

 

3. filename에 현재 디렉토리를 나타내는 "."을 먼저 복사하고, 그 뒤에 URI를 이어 붙인다. 이렇게 함으로써 서버의 로컬 파일 시스템 상에서 해당 파일의 경로를 생성한다.

 

4. URI가 슬래시(/)로 끝난다면, 이는 디렉토리에 대한 요청으로 간주되며, 기본 파일 이름(예: "adder.html")을 filename에 추가한다.

 

동적 콘텐츠 처리:

1. URI에 "cgi-bin"이 포함되어 있다면, 요청은 동적 콘텐츠로 간주된다.

 

2. index 함수를 사용하여 URI 내의 '?' 문자를 찾는다. 이 문자는 CGI 인자의 시작을 나타낸다.

 

3. '?' 문자가 존재한다면, 이를 기준으로 문자열을 나눈다. '?' 뒤의 문자열은 CGI 인자로, cgiargs에 복사된다. '?' 문자 자체는 '\0'(null 문자)로 대체되어, URI 문자열을 종료시킨다.

 

4. filename에 현재 디렉토리를 나타내는 "."을 복사하고, 변경된 URI(인자가 제거된 상태)를 이어 붙인다.

 

tiny.c(clienterror)

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg){
  char buf[MAXLINE], body[MAXBUF];

  /* Build the HTTP response body*/
  sprintf(body, "<html><title>Tiny Error</title>");
  sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
  sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
  sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
  sprintf(body, "%s<hr><em>The Tiny Web Server</em>\r\n", body);

  /* Print the HTTP response */
  sprintf(buf, "HTTP/1.1 %s %s\r\n", errnum, shortmsg);
  Rio_writen(fd, buf, strlen(buf));
  sprintf(buf, "Content-type: text/html\r\n");
  Rio_writen(fd, buf, strlen(buf));
  sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));

  Rio_writen(fd, buf, strlen(buf));
  Rio_writen(fd, body, strlen(body));
}
/* $end clienterror */
void echo(int connfd)
{
  size_t n;
  char buf[MAXLINE];
  rio_t rio;

  Rio_readinitb(&rio, connfd);
  while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
  {
    if (strcmp(buf, "\r\n") == 0)
      break;
    Rio_writen(connfd, buf, n);
  }
}

 

sprintf를 사용해 HTML 형식의 오류 메시지를 body 버퍼에 작성한다. 오류 제목, 배경색, HTTP 상태 코드, 짧은 오류 메시지, 긴 오류 메시지, 오류 원인이 포함된다.

 

오류 상태 코드와 짧은 메시지를 포함하는 HTTP 응답의 시작 라인을 buf 버퍼에 작성한다. 컨텐츠 타입(text/html)을 지정하고, 계산된 응답 본문의 길이를 사용해 Content-length 헤더를 작성한다.

 

adder.c

#include "csapp.h"

int main(void) {
  char *buf, *p;
  char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
  int n1=0, n2=0;

  /* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL) {
  p = strchr(buf, '&');
  *p = '\0';
  sscanf(buf, "first=%d", &n1);
  sscanf(p+1, "second=%d", &n2);
}

  /* Make the response body */
  sprintf(content, "QUERY_STRING=%s", buf);
  sprintf(content, "Welcome to add.com: ");
  sprintf(content, "%sTHE Internet addition portal. \r\n<p>", content);
  sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>", content, n1, n2, n1 + n2);
  sprintf(content, "%sThanks for visiting!\r\n", content);

  /* Generate the HTTP response */
  printf("Connection: close\r\n");
  printf("Content-length: %d\r\n", (int)strlen(content));
  printf("Content-type: text/html\r\n\r\n");
  printf("%s", content);
  fflush(stdout);

  exit(0);
}

 

1. getenv("QUERY_STRING")를 통해 환경변수에서 쿼리 문자열(QUERY_STRING)을 가져온다. 이 문자열은 웹 브라우저(또는 다른 클라이언트)에서 입력한 데이터를 포함하며, first=숫자1&second=숫자2 형식으로 되어 있다.

 

2. 쿼리 문자열에서 & 문자를 찾아 두 부분으로 나눈다(first=숫자1second=숫자2). 이후, sscanf를 사용해 각 부분에서 숫자를 추출하고, 정수 변수 n1, n2에 저장한다.

 

3. 응답 본문을 구성하는 여러 sprintf 호출을 통해 HTML 형식의 응답을 만든다. 이 응답은 방문자에게 환영 메시지, 계산된 결과, 그리고 방문에 대한 감사 메시지를 포함한다.

 

4. printf를 사용해 HTTP 헤더를 출력한다. 여기에는 연결 종료(Connection: close), 컨텐츠 길이(Content-length), 컨텐츠 타입(Content-type: text/html)이 포함된다. 마지막으로, 만들어진 HTML 컨텐츠 본문을 출력한다.

 

5. fflush(stdout)를 호출해 출력 버퍼를 비우고, exit(0)을 호출해 프로그램을 정상적으로 종료한다.

 

이 코드는 예전에 구현한 코드이고 지금 보니, adder.c 코드에는 두 가지 문제점이 있다.

 

1. 현재 코드에는 sprintf(content, "QUERY_STRING=%s", buf);라는 라인이 있으나, 이후에 content에 다른 문자열을 쓸 때마다 이 내용을 덮어쓰게 되므로, 실제로는 이 내용이 응답에 포함되지 않는다. 적절한 응답 메시지 생성을 위해 sprintf 대신 strcat을 사용하거나, sprintf를 사용할 때 content에 계속해서 추가하는 방식으로 바꿔야 한다.

 

2. *p = '\0'; 이 부분에서 pNULL인 경우(즉, 쿼리 문자열에 &가 없는 경우)를 처리하지 않고 있다. 이 경우 p를 참조하려고 하면 프로그램이 비정상적으로 종료될 수 있다. 따라서 pNULL이 아닌지 확인하는 조건문이 필요하다.

 

위의 모든 코드는 yunsejin/tiny에 있다.

 

GitHub - yunsejin/Webproxy-lab

Contribute to yunsejin/Webproxy-lab development by creating an account on GitHub.

github.com

 

728x90

'서버' 카테고리의 다른 글

Express.js Clean Architecture 구성하기  (0) 2024.04.12
Proxy 서버 구현  (1) 2024.03.24
Socket, Redirection, Pipe  (1) 2024.03.23
Parsing, Caching, Filtering, Load Balancing, MTU, NAT  (1) 2024.03.23
728x90

리다이렉션(Redirection)명령어의 출력을 터미널이 아닌 다른 곳으로 보내는 기능이다. 일반적으로는 파일로의 출력이나, 파일로부터의 입력을 처리하는 데 사용된다.

 

사용의 예로, 소켓을 통해 수신된 데이터를 파일로 저장하거나, 파일의 데이터를 소켓을 통해 전송할 때 리다이렉션을 사용할 수 있다.

 

> : 명령어의 출력을 파일로 전송한다. 이미 존재하는 파일인 경우 내용을 덮어쓴다.

예: ls > filelist.txt는 현재 디렉토리의 파일 목록을 filelist.txt 파일에 저장한다.

 

>> : 명령어의 출력을 파일에 추가한다. 파일이 존재하지 않는 경우 새로 생성한다.

예: echo "새로운 내용" >> filelist.txt는 "새로운 내용"을 filelist.txt 파일의 끝에 추가한다.

 

< : 파일의 내용을 명령어의 입력으로 사용한다.

예: sort < unsorted.txt는 unsorted.txt 파일의 내용을 정렬하여 출력한다.

 

파이프(Pipe)는 한 명령어의 출력을 다른 명령어의 입력으로 직접 연결한다. 이를 통해 여러 명령어를 연결하여 복잡한 처리를 한 줄의 명령어로 수행할 수 있다.

 

네트워크를 통해 전송된 데이터를 파이프를 사용해 여러 단계의 처리 과정을 거친 후 최종적으로 원하는 결과를 얻을 수 있다. 예를 들어, 네트워크로부터 수신된 데이터를 파싱하고, 필터링하고, 변환하는 등의 작업을 파이프라인을 구성하여 처리할 수 있다.

 

| : 앞의 명령어 출력을 뒤의 명령어의 입력으로 전달한다.

예: ls | sort는 현재 디렉토리의 파일 목록을 정렬하여 출력한다.

+예: cat filelist.txt | grep "특정 문자열"은 filelist.txt 파일에서 "특정 문자열"을 포함하는 라인만을 검색하여 출력한다.

 

소켓(Socket)네트워크를 통한 데이터 송수신을 위한 인터페이스이다. 소켓을 사용하면, 프로그램은 네트워크 상의 다른 시스템과 데이터를 주고받을 수 있다. 리다이렉션과 파이프는 로컬 시스템 내에서 데이터의 흐름을 제어하는 데 사용되는 반면, 소켓은 네트워크를 통해 떨어진 위치에 있는 시스템 간의 데이터 흐름을 제어한다.

 

서버와 클라이언트 간의 소켓 통신은 네트워크 통신의 기본적인 모델 중 하나로, 데이터를 교환하기 위한 연결을 설정하고 유지하는 방식이다. 소켓은 네트워크 상에서 데이터를 송수신하기 위한 엔드포인트로, IP 주소와 포트 번호로 정의된다.

 

서버:

  1. 소켓 생성: 서버는 socket() 함수를 호출하여 소켓을 생성한다.
  2. 소켓 바인딩: 생성된 소켓에 IP 주소와 포트 번호를 할당하기 위해 bind() 함수를 사용한다. 이는 서버의 주소로 사용된다.
  3. 리스닝: listen() 함수를 호출하여 서버 소켓이 클라이언트의 연결 요청을 대기하도록 설정한다.
  4. 연결 수락: 클라이언트로부터의 연결 요청이 들어오면, accept() 함수를 호출하여 연결을 수락한다. 이 함수는 새로운 소켓을 반환하며, 이 소켓을 통해 클라이언트와 통신한다.

클라이언트:

  1. 소켓 생성: 클라이언트도 socket() 함수를 호출하여 소켓을 생성한다.
  2. 서버에 연결: connect() 함수를 사용하여 서버의 IP 주소와 포트 번호로 설정된 서버 소켓에 연결을 시도한다.

데이터 송수신:

  • 데이터 전송: 연결이 설정되면, 서버와 클라이언트는 send() 또는 recv() 함수(또는 write(), read() 함수)를 사용하여 데이터를 송수신한다.
  • 통신 종료: 데이터 송수신이 완료되면, close() 함수를 호출하여 소켓 연결을 종료한다.

 

728x90

'서버' 카테고리의 다른 글

Express.js Clean Architecture 구성하기  (0) 2024.04.12
Proxy 서버 구현  (1) 2024.03.24
echo server, tiny server 구현  (2) 2024.03.24
Parsing, Caching, Filtering, Load Balancing, MTU, NAT  (1) 2024.03.23
728x90

파싱(Parsing)은 주어진 데이터(예: 웹 페이지, 파일, 메시지 등)를 분석하여 그 구조를 이해하고, 필요한 정보를 추출하거나, 다른 형태로 변환하는 과정을 말한다. 웹에서는 URL, HTML, JSON, XML 등의 데이터를 분석하여 웹 페이지를 렌더링하거나, API 응답을 처리하기 위해 파싱을 사용한다. 예를 들어, 프록시 서버는 HTTP 요청의 URI를 파싱하여 목적지 서버, 경로, 파라미터 등을 분석한다.

 

캐싱(Caching)은 데이터나 결과물을 임시 저장하는 기술로, 빠른 데이터 접근을 가능하게 하여 성능을 향상시킨다. 웹 캐싱은 자주 요청되는 웹 페이지, 이미지, 기타 파일을 저장해두고, 같은 요청이 들어올 때마다 빠르게 제공하여 서버의 부하를 줄이고 사용자 경험을 개선한다. 프록시 서버나 브라우저 자체에 캐싱 기능이 있을 수 있다.

 

필터링(Filtering)은 특정 조건에 따라 데이터나 트래픽을 걸러내는 과정을 말한다. 웹 필터링은 부적절한 웹사이트 접근을 차단하거나, 특정 유형의 인터넷 트래픽(예: 광고)을 제한하기 위해 사용된다. 기업이나 학교에서는 보안, 생산성 향상, 네트워크 사용 규제 목적으로 필터링을 사용한다.

 

로드 밸런싱(Load Balancing)은 네트워크 트래픽이나 요청을 여러 서버나 리소스에 균등하게 분배하여, 어느 한 곳에 과부하가 걸리지 않도록 하는 기술이다. 이를 통해 시스템의 가용성과 내구성을 높이고, 사용자에게 일관된 서비스 품질을 제공할 수 있다. 로드 밸런서는 여러 서버 사이에서 클라이언트의 요청을 적절히 분배하는 역할을 한다.

 

MTU(Maximum Transmission Unit)네트워크를 통해 전송될 수 있는 최대 데이터 패킷 크기를 말한다. MTU는 네트워크 인터페이스나 프로토콜마다 다를 수 있으며, 이는 특정 네트워크 링크를 통해 한 번에 전송할 수 있는 최대 바이트 수를 지정한다. MTU의 크기는 네트워크의 효율성과 성능에 영향을 미칠 수 있다.

 

 

너무 작은 MTU는 네트워크 오버헤드를 증가시키고, 너무 큰 MTU는 패킷 손실률을 높일 수 있다. 적절한 MTU 설정은 네트워크 성능을 최적화하는 데 중요하다.

다른 네트워크 세그먼트 간에 MTU 크기가 다를 경우, 데이터가 올바르게 전송되기 위해서는 MTU 크기를 조정하거나 패킷을 분할하여야 한다. 이 과정을 MTU 발견이라고 한다.

TCP/IP 네트워크는 MTU 발견 메커니즘을 사용하여, 데이터가 전송되는 경로의 최소 MTU 값을 자동으로 찾을 수 있다. 이를 통해 패킷 분할이 필요한 경우 최적의 크기로 조정할 수 있다.

일반적으로 TCP헤더와 IP헤더는 각각 20바이트를 차지한다. 스트림 데이터를 1460씩 잘라서, 각각 TCP와 IP헤더를 붙이면 패킷이 1500byte가 된다.

작은 데이터를 보낼 때마다 패킷을 보내면 패킷이 너무 낭비다. 그래서 버퍼에 데이터를 최대한 채워서 보내야 하는데, 이 때 쓰는 알고리즘이 nagle(네이글) 알고리즘이라 한다.

Network Address Translation (NAT)

인터넷 프로토콜(IP) 주소를 다른 주소로 변환하는 과정이다. 이 기술은 주로 IP 주소의 부족 문제를 해결하고, 네트워크 보안을 강화하기 위해 사용된다. NAT는 일반적으로 라우터나 방화벽과 같은 네트워크 장비에서 구현된다.

 

NAT을 사용하면 하나의 공인 IP 주소를 여러 개의 사설 IP 주소와 매핑하여 사용할 수 있다. 이를 통해 인터넷에 연결된 장치들이 공인 IP 주소를 공유하여 사용할 수 있게 된다.

NAT를 통해 내부 네트워크의 구조와 IP 주소를 외부에 숨길 수 있다. 이는 내부 네트워크에 대한 무단 접근을 어렵게 만들어 네트워크 보안을 강화한다.

NAT를 사용하면 내부 네트워크의 IP 주소 체계를 인터넷의 IP 주소 체계와 독립적으로 운영할 수 있다. 이는 IP 주소의 재할당 및 네트워크 구성 변경을 용이하게 한다.

 

++ARP와 RARP

ARP : 네트워크 상의 장치의 IP 주소를 해당 장치의 물리적 MAC 주소로 변환하는 것이다. 이는 IP 네트워크 계층에서 전송 계층으로의 데이터 전송을 가능하게 한다.

RARP : 주요 목적은 네트워크 상의 장치의 물리적 MAC 주소를 IP 주소로 변환하는 것이다. 이는 주로 부팅 과정에서 디스크리스 스테이션(디스크 드라이브가 없는 컴퓨터)이나 네트워크 상의 장치가 자신의 IP 주소를 알아내는 데 사용된다.

728x90

'서버' 카테고리의 다른 글

Express.js Clean Architecture 구성하기  (0) 2024.04.12
Proxy 서버 구현  (1) 2024.03.24
echo server, tiny server 구현  (2) 2024.03.24
Socket, Redirection, Pipe  (1) 2024.03.23

+ Recent posts