Interview/Network

URL Parsing 내부 로직 C, Golang 구현

김 정출 2024. 9. 29. 10:17

URL Parsing 내부 로직 구현

URL 파싱은 주어진 URL을 파트별로 나누는 작업입니다. URL은 다음과 같은 형식을 따릅니다.

scheme://host:port/path?query#fragment

각 부분을 나누어 추출하는 C 코드 예제를 아래에 작성하였습니다.

URL 파싱 C 코드

#include 
#include 
#include 

#define MAX_URL_LEN 2048

// Structure to store parsed URL components
typedef struct {
    char scheme[32];
    char host[256];
    int port;
    char path[512];
    char query[256];
    char fragment[256];
} URL;

// Function to parse the URL
int parse_url(const char *url, URL *parsed_url) {
    // Copy URL into a modifiable string
    char temp_url[MAX_URL_LEN];
    strncpy(temp_url, url, MAX_URL_LEN);

    // Initialize parsed URL structure
    memset(parsed_url, 0, sizeof(URL));
    parsed_url->port = -1; // Default port to -1 if not found

    // Parse the scheme (e.g., http, https)
    char *scheme_end = strstr(temp_url, "://");
    if (scheme_end == NULL) {
        return -1; // Invalid URL (scheme not found)
    }
    *scheme_end = '\\\\0'; // Null-terminate scheme
    strncpy(parsed_url->scheme, temp_url, sizeof(parsed_url->scheme));
    char *remainder = scheme_end + 3; // Move past '://'

    // Parse the host
    char *host_end = strpbrk(remainder, ":/?#");
    if (host_end == NULL) {
        // No path/query/fragment, this is the end of the URL
        strncpy(parsed_url->host, remainder, sizeof(parsed_url->host));
        return 0;
    }
    strncpy(parsed_url->host, remainder, host_end - remainder);

    // Parse the port if present
    if (*host_end == ':') {
        char *port_start = host_end + 1;
        char *port_end = strpbrk(port_start, "/?#");
        if (port_end == NULL) {
            parsed_url->port = atoi(port_start); // No path/query/fragment
            return 0;
        }
        char port_str[6];
        strncpy(port_str, port_start, port_end - port_start);
        port_str[port_end - port_start] = '\\\\0';
        parsed_url->port = atoi(port_str);
        host_end = port_end; // Update the host_end to the path/query start
    }

    // Parse the path if present
    if (*host_end == '/') {
        char *path_end = strpbrk(host_end, "?#");
        if (path_end == NULL) {
            // No query/fragment
            strncpy(parsed_url->path, host_end, sizeof(parsed_url->path));
            return 0;
        }
        strncpy(parsed_url->path, host_end, path_end - host_end);
        host_end = path_end; // Move to the query/fragment start
    }

    // Parse the query if present
    if (*host_end == '?') {
        char *query_end = strchr(host_end, '#');
        if (query_end == NULL) {
            // No fragment
            strncpy(parsed_url->query, host_end + 1, sizeof(parsed_url->query));
            return 0;
        }
        strncpy(parsed_url->query, host_end + 1, query_end - (host_end + 1));
        host_end = query_end; // Move to the fragment start
    }

    // Parse the fragment if present
    if (*host_end == '#') {
        strncpy(parsed_url->fragment, host_end + 1, sizeof(parsed_url->fragment));
    }

    return 0;
}

int main() {
    const char *url = "<https://example.com:8080/path/to/resource?query=value#fragment>";
    URL parsed_url;

    if (parse_url(url, &parsed_url) == 0) {
        printf("Scheme: %s\\\\n", parsed_url.scheme);
        printf("Host: %s\\\\n", parsed_url.host);
        printf("Port: %d\\\\n", parsed_url.port);
        printf("Path: %s\\\\n", parsed_url.path);
        printf("Query: %s\\\\n", parsed_url.query);
        printf("Fragment: %s\\\\n", parsed_url.fragment);
    } else {
        printf("Failed to parse URL.\\\\n");
    }

    return 0;
}

설명:

  1. URL 구조체: URL의 각 부분을 저장하는 구조체 URL을 정의했습니다. 여기에는 scheme, host, port, path, query, fragment 등이 포함됩니다.
  2. 파싱 함수: parse_url 함수는 주어진 URL을 파싱하여 URL 구조체에 각 부분을 저장합니다.
  3. 포인터 사용: C 언어에서는 문자열을 처리할 때 포인터와 strpbrk, strstr 등의 함수를 사용하여 자를 찾고, 그 위치를 기준으로 문자열을 자릅니다.
  4. 테스트 URL: main 함수에서 파싱할 URL을 지정하고, 파싱 결과를 출력합니다 https://example.com:8080/path/to/resource?query=value#fragment와 같은 URL을 처리하며, 각 부분을 잘라내어 출력합니다.

Go에서 표준 패키지인 net/url을 사용하지 않고 URL 파싱을 직접 구현하려면, 문자열을 분석하여 URL의 각 부분을 추출해야 합니다. 이는 C언어와 유사한 방식으로, 문자열 탐색 및 구분자를 사용해 URL의 구조를 직접 분해하는 방식입니다.

아래는 Go에서 URL을 수동으로 파싱하는 코드를 작성한 예시입니다.

URL 파싱 Go 코드 (패키지 없이 직접 구현)

package main

import (
	"fmt"
	"strings"
)

// URL 구조체
type URL struct {
	Scheme   string
	Host     string
	Port     string
	Path     string
	Query    string
	Fragment string
}

// URL 파싱 함수
func parseURL(rawURL string) (*URL, error) {
	var parsedURL URL

	// 1. 스킴 (scheme) 분리
	schemeEnd := strings.Index(rawURL, "://")
	if schemeEnd == -1 {
		return nil, fmt.Errorf("invalid URL: scheme not found")
	}
	parsedURL.Scheme = rawURL[:schemeEnd]
	rawURL = rawURL[schemeEnd+3:]

	// 2. 호스트와 포트 분리
	hostEnd := strings.IndexAny(rawURL, "/?#")
	if hostEnd == -1 {
		hostEnd = len(rawURL)
	}
	hostPort := rawURL[:hostEnd]
	rawURL = rawURL[hostEnd:]

	// 호스트와 포트 분리
	colonPos := strings.LastIndex(hostPort, ":")
	if colonPos != -1 {
		parsedURL.Host = hostPort[:colonPos]
		parsedURL.Port = hostPort[colonPos+1:]
	} else {
		parsedURL.Host = hostPort
		parsedURL.Port = "" // 포트가 없을 때는 빈 문자열
	}

	// 3. 경로 (path) 분리
	pathEnd := strings.IndexAny(rawURL, "?#")
	if pathEnd == -1 {
		pathEnd = len(rawURL)
	}
	parsedURL.Path = rawURL[:pathEnd]
	rawURL = rawURL[pathEnd:]

	// 4. 쿼리 (query) 분리
	if len(rawURL) > 0 && rawURL[0] == '?' {
		queryEnd := strings.Index(rawURL, "#")
		if queryEnd == -1 {
			queryEnd = len(rawURL)
		}
		parsedURL.Query = rawURL[1:queryEnd]
		rawURL = rawURL[queryEnd:]
	}

	// 5. 프래그먼트 (fragment) 분리
	if len(rawURL) > 0 && rawURL[0] == '#' {
		parsedURL.Fragment = rawURL[1:]
	}

	return &parsedURL, nil
}

func main() {
	rawURL := "<https://example.com:8080/path/to/resource?query=value#fragment>"
	parsedURL, err := parseURL(rawURL)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// 결과 출력
	fmt.Printf("Scheme: %s\\\\n", parsedURL.Scheme)
	fmt.Printf("Host: %s\\\\n", parsedURL.Host)
	fmt.Printf("Port: %s\\\\n", parsedURL.Port)
	fmt.Printf("Path: %s\\\\n", parsedURL.Path)
	fmt.Printf("Query: %s\\\\n", parsedURL.Query)
	fmt.Printf("Fragment: %s\\\\n", parsedURL.Fragment)
}

설명:

  1. parseURL 함수: 주어진 URL 문자열을 수동으로 파싱하는 함수입니다. 각 단계에서 URL의 일부(스킴, 호스트, 포트, 경로, 쿼리, 프래그먼트)를 찾아내고 이를 URL 구조체에 저장합니다.
  2. 스킴 파싱: ://를 기준으로 스킴을 분리합니다. 스킴이 없다면 잘못된 URL로 처리합니다.
  3. 호스트 및 포트 파싱: 호스트와 포트를 /, ?, # 중 첫 번째 구분자를 기준으로 찾아내며, 마지막 :를 찾아 포트를 구분합니다.
  4. 경로 파싱: 쿼리나 프래그먼트 전에 나타나는 경로를 분리합니다.
  5. 쿼리 파싱: ? 이후부터 # 이전까지의 부분을 쿼리로 처리합니다.
  6. 프래그먼트 파싱: # 이후의 부분을 프래그먼트로 처리합니다.

실행 결과:

https://example.com:8080/path/to/resource?query=value#fragment라는 URL을 입력으로 주었을 때, 출력은 다음과 같습니다.

Scheme: https
Host: example.com
Port: 8080
Path: /path/to/resource
Query: query=value
Fragment: fragment

이렇게 표준 패키지를 사용하지 않고 문자열 처리만으로도 URL을 직접 파싱할 수 있습니다.