레이블이 C언어인 게시물을 표시합니다. 모든 게시물 표시
레이블이 C언어인 게시물을 표시합니다. 모든 게시물 표시

2018년 10월 29일 월요일

extern "C"

C 와 다르게 C++ 은 함수 overloading 을 지원하기 위해 컴파일시 최종 함수 이름에 변형을 가한다.

그러므로 C++ 로 작성한 DLL 등의 동적 라이브러리를 사용하는 경우 header 에 지정된 함수 이름으로 함수를 찾지 못하는 문제가 발생한다.

그러나 extern "C" { ... } 로 감싼 부분의 코드는 C++ compiler 가 C compiler 처럼 함수 이름에 변형을 가하지 않기 때문에 다른 프로그램이 해당 이름으로 함수를 호출할 수 있다.

예)

#ifdef __cplusplus
extern "C" {
#endif

void hello(void);

#ifdef __cplusplus
}
#endif

C++ compiler 는 __cplusplus 값을 미리 선언하므로 해당 값이 선언되었는지 확인하여 C++ compiler 의 경우한 extern "C" 로 감싸도록 한다.

보통 header 파일은 동적 라이브러리와 함께 배포된다.

extern "C" 영역 안에는 C 코드로만 작성되어야만 하기 때문에 C++ 에서만 사용되는 class 등의 키워드는 포함되면 안된다.

extern "C" 영역의 코드는 C compiler 로 컴파일 할 때도 문제 없도록 c++ 코드가 없도록 해야 한다.


2015년 12월 29일 화요일

C 언어 - 네트워크 프로그래밍 (HTTP 서버 예제)

HTTP 서버 예제


설명할 예제


github: https://github.com/bjtj/tjsamples/tree/master/linux/network/simplehttpserver
  • simple-http-server.c
  • http-parser.c
  • http-parser.h
  • single-thread-http-server.c

clone 받기

$ git clone https://github.com/bjtj/tjsamples

HTTP 프로토콜


HTTP 패킷 구조


HTTP 패킷은 요청(request) 와 응답(response) 로 구분되며 요청 및 응답이 동일한 구조를 갖는다.


  • HTTP header
  • HTTP content (content 는 없을 수도 있음)


HTTP Header 구조


First line\r\n
Header field\r\n
...
\r\n
\r\n 로 줄이 구분된다.
줄은 각 의미 단위이다.
Header field 는 이름/값 형태의 묶음으로 여러 개로 확장 가능하다.
header 의 끝은 \r\n 을 하나 더 추가해 표시한다.

예) 요청

GET / HTTP/1.1\r\n
User-Agent: curl/7.35.0\r\n
Host: example.com\r\n
Accept: */*\r\n
\r\n

예) 응답

HTTP/1.1 200 OK\r\n
Accept-Ranges: bytes\r\n
Cache-Control: max-age=604800\r\n
Content-Type: text/html\r\n
Date: Wed, 30 Dec 2015 02:17:52 GMT\r\n
Etag: "359670651+gzip"\r\n
Expires: Wed, 06 Jan 2016 02:17:52 GMT\r\n
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT\r\n
Server ECS (cpm/F9D5) is not blacklisted\r\n
Server: ECS (cpm/F9D5)\r\n
Vary: Accept-Encoding\r\n
X-Cache: HIT\r\n
x-ec-custom-error: 1\r\n
Content-Length: 1270\r\n
\r\n
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
single-thread-http-server
(생략)

First line 구조

<의미1> <의미2> <의미3>\r\n
3가지 의미가 공백으로 구분되어 있다.
요청과 응답에 따라 의미가 다르다.

요청

  • 의미1 : method
  • 의미2 : uri
  • 의미3 : HTTP 프로토콜 버전


응답

  • 의미1 : HTTP 프로토콜 버전
  • 의미2 : 숫자 형태의 응답 상태 값
  • 의미3 : 문자열 형태의 응답 상태 (공백이 포함될 수 있음)


Header field 구조


<이름>: <값>\r\n
예를 들어 Content-Length: 0\r\n 식으로 HTTP content 에 대한 정보 및 여러 통신에 필요한 부가 정보를 header field 로 표현한다.

표준 header fields

참고: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
  • Content-Type : HTTP content 의 형태 (예: 일반 문서는 text/plain, html 문서는 text/html 등으로 표현)
  • Content-Length : HTTP content 의 크기 (byte 단위)


HTTP 서버 구현


아래 예제들은 linux 환경에서 구현됨

간단한 예제로 시작




소스 내용은 어떠한 요청이든 아래 응답을 보내고 종료하는 HTTP 서버이다.

HTTP/1.1 200 OK\r\n
Content-Length: 5\r\n
Content-Type: text/plain\r\n
\r\n
Hello

위 상태로는 request 에 적절히 대응하기 어려운 면이 있다.

수정 필요한 부분


  • 여러 요청 처리
  • 현재 구현은 recv 한번으로 HTTP header 를 모두 받도록 되어 있지만 TCP 통신 특성 상 데이터가 파편화 되어 수신 될 수 있음
  • HTTP header 를 먼저 받아 content 처리 방법이 선택되어야 한다.


수정한 소스




while (1) 으로 무한 루프 돌면서 accept() 및 s_handle_client() 에서 요청 처리


s_handle_client() 에서 1바이트씩 읽어가며 header 부분을 읽도록 수정


HTTP header parse 는 덤으로 구현함


2015년 12월 28일 월요일

C 언어 - 네트워크 프로그래밍 (IPv4 와 IPv6 대응)

IPv4 와 IPv6


IPv4 주소의 예

  • 192.168.0.1


IPv6 주소의 예

  • 2001:0DB8:0000:0000:0000:0000:1428:57ab


Socket API 상 주소는 sockaddr 구조체로 대표되며 IPv4 는 sockaddr_in 에 담아야 하고 IPv6 는 sockaddr_in6 에 담아야 한다.

IPv6 에서 주소 값의 확장 등의 이유로 sockaddr_in 과 sockaddr_in6 는 구조체 모양이나 크기가 다르다.

sockaddr_in 과 sockaddr_in6 는 모두 family 라는 멤버 변수가 포함되어 있고 Socket API 는 이 family 값을 확인하여 해당 구조체가 sockaddr_in 인지 sockaddr_in6 인지 확인할 수 있다.

IPv4 와 IPv6 를 대응하기 위한 Socket API 들이 이미 존재한다.


  • getaddrinfo()
  • inet_pton()
  • inet_ntop()

API 설명



getaddrinfo()


int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);

참고: http://man7.org/linux/man-pages/man3/getaddrinfo.3.html

매개변수:


  • node : IP 주소 등
  • service : Port 번호 등
  • hints : node 와 service 를 이용해 결과 값을 만들기 위한 정보
  • res : 결과값 (반드시 freeaddrinfo() 로 메모리 해제해야 함)


설명:

기존의 sockaddr_in 을 직접 입력하는 방식이 아닌 node 와 service 및 hints 를 이용해 socket() 함수 및 bind() 함수에 사용할 수 있는 정보를 얻을 수 있다.
node 와 service 는 일반적으로 IP주소와 Port번호로 대입되며 NULL 을 입력하면 해당 부분은 INADDR_ANY 와 유사한 효과를 볼 수 있다.
장점은 node 에 IPv4 나 IPv6 주소 또는 domain 이름 (예: www.google.com) 등 어떠한 주소 체계도 처리해 준다.
해당 함수로 인해 개발자가 대응할 부분이 많이 줄어들었다.

예) getaddrinfo() 를 이용해 remote 서버의 주소 정보를 얻고 그에 맞는 socket 을 생성
struct addrinfo hints;struct addrinfo * res = NULL;int sock;char buffer[1024] = {0,};
memset(&hints, 0, sizeof(hints));hints.ai_family = AF_UNSPEC;hints.ai_socktype = SOCK_STREAM;if (getaddrinfo("127.0.0.1", "9000", &hints, &res) < 0) { perror("getaddrinfo()"); return 1;}sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);if (sock < 0) { perror("socket()"); return 1;}
(생략)
freeaddrinfo(res);

inet_pton()


int inet_pton(int af, const char *src, void *dst);

참고: http://man7.org/linux/man-pages/man3/inet_pton.3.html

매개변수:


  • af : AF_INET 또는 AF_INET6
  • src : IPv4 또는 IPv6 주소
  • dst : src 가 변환되어 채워질 addr 구조체 (in_addr 또는 in6_addr)


설명:

사람이 읽을 수 있는 192.168.0.1 또는 2001:0DB8:0000:0000:0000:0000:1428:57ab 등의 주소를 컴퓨터가 인식할 수 있는 주소 형태로 변경

inet_ntop()


const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

참고: http://man7.org/linux/man-pages/man3/inet_ntop.3.html

매개변수:


  • af : AF_INET 또는 AF_INET6
  • src : addr 구조체 (in_addr 또는 in6_addr)
  • dst : src 가 변환되어 채워질 문자열 버퍼
  • size : 문자열 버퍼의 크기


설명:

inet_pton() 과 반대로 in_addr 또는 in6_addr 을 사람이 읽을 수 있는 문자열로 변환

Cross platform


위 함수들은 winsock API 에도 동일하게 존재한다. (변수 타입이 미묘하게 다를 수는 있음)
Windows XP 의 경우 IPv4 대응 함수인 inet_aton() 과 inet_ntoa() 만 사용 가능하다.


2015년 12월 18일 금요일

C 언어 - 네트워크 프로그래밍 (Socket API)

Socket API


Socket API 를 이용해 네트워크 프로그램을 구현할 수 있다.

설명할 API 종류

  • socket() : socket handler 생성
  • close() : socket handle 해제
  • bind() : IP 및 port 할당
  • listen() : accept 에서 사용될 connection 대기열 설정
  • accept() : connection 대기열에서 connection 을 가져옴
  • connect() : server 로 연결
  • setsockopt() : 다양한 소켓 설정
  • getsockopt() : 소켓 설정 정보를 취득

 

전제 조건

유닉스, 리눅스 계열:

#include <arpa/inet.h>
#include <sys/socket.h>


윈도우즈:

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma comment(lib, "Ws2_32.lib")


WSADATA 초기화 및 해제

  • 프로그램 시작과 끝에 초기화와 해제 코드를 삽입

static WSADATA wsaData;

WSAStartup(MAKEWORD(2, 2), &wsaData);

...(프로그램 코드)...

WSACleanup();


socket


int socket(int domain, int type, int protocol);

http://man7.org/linux/man-pages/man2/socket.2.html

파라메터:
  • domain : 우선 AF_INET 와 AF_INET6 만 알아두자
  • type : 우선 SOCK_STREAM 과 SOCK_DGRAM 만 알아두자
  • protocol : 우선 IPPROTO_TCP 와 IPPROTO_UDP 만 알아주자

설명:
  • socket handle 을 하나 할당 받는다.
  • socket 옵션들도 많지만 우선 TCP/UDP 와 IPv4/IPv6 만 고려하자.
    domain 에 AF_INET 를 입력하면 IPv4 socket, AF_INET6 를 입력하면 IPv6 socket 이 생성된다.
  • type 에 SOCK_STREAM 을 입력하면 TCP socket, SOCK_DGRAM 을 입력하면 UDP socket 이 생성된다.
  • protocol 에 TCP/UDP 관계없이 0 을 입력하여도 무방하고 IPPROTO_TCP/IPPROTO_UDP 를 TCP/UDP 구분하여 입력하여도 된다.

예) IPv4 TCP socket 생성
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock < 0) {
    // error
}

close


int close(int fd);

http://man7.org/linux/man-pages/man2/close.2.html

파라메터:
  • fd : socket() 으로 생성된 핸들
설명:
  • socket() 함수로 얻은 handle 을 해소

예)
close(sock);

bind


int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

http://man7.org/linux/man-pages/man2/bind.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • addr : bind 할 주소 구조체
  • addrlen : 주소 구조체의 크기
설명:
  • socket handle 에 network interface 와 port 를 할당 한다.
  • addr 파라메터에 bind 원하는 IP 주소 및 port 정보를 채운 구조체를 입력한다.
  • struct sockaddr 은 IPv4 와 IPv6 에 따라 구조체가 확장된다.
    • IPv4 는 struct sockaddr_in 구조체를 사용
    • IPv6 는 struct sockaddr_in6 구조체를 사용


예) IPv4 주소에 할당
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);

if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
    // error
}

    • INADDR_ANY : 기본 network interface 의 IP 주소 (개발자가 고려할 필요없이 운영체제 기본 network interface 에 할당됨)
    • htonl() : host to network long 이라 풀어 읽을 수 있겠다.
    • htons() : host to network short 이라 풀어 읽을 수 있겠다.

listen


int listen(int sockfd, int backlog); 

http://man7.org/linux/man-pages/man2/listen.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • backlog : connection 대기열 크기
설명:
  • accept() 를 하기 위해 client 접속 요청 대기열의 크기를 정한다.

예)
if (listen(sock, 5) != 0) {
    // error
}

accept


int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

http://man7.org/linux/man-pages/man2/accept.2.html

  • sockfd : socket() 으로 생성된 핸들
  • addr : client 의 주소가 담길 구조체
  • addrlen : client 주소 구조체의 크기 
설명:
  • client 접속 요청 대기열에서 client 정보를 하나 가져온다.
  • 대기열에 client 접속 요청이 없으면 요청이 있을 때까지 기다린다.

예) IPv4 주소로 bind 한 경우
struct sockaddr_in client_addr;
socklen_t client_addr_len;
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sock < 0) {
    // error
}

    • client_addr 에 client 의 IP 및 port 주소가 담겨있다.

connect


int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

http://man7.org/linux/man-pages/man2/connect.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • addr : 연결하고자 하는 remote 주소 구조체
  • addrlen : remote 주소 구조체 크기 
설명:

  • Server 소켓은 bind(), listen(), accept() 함수의 호출로 client 접속 요청을 대기하는 반면 client 소켓은 connect() 로 대기하고 있는 server 에 접속 한다.


예) IPv4 server 로 접속 요청
struct sockaddr_in remote_addr;
memset(&remote_addr, 0, sizeof(remote_addr));
remote_addr.sin_family = AF_INET;
remote_addr.sin_addr.s_addr = inet_addr("192.168.0.1");
remote_addr.sin_port = htons(port);
if (connect(sock, (struct sockaddr*)&remote_addr, sizeof(remote_addr)) != 0) {
    // error
}


setsockopt


int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 

http://man7.org/linux/man-pages/man2/setsockopt.2.html

  • sockfd : socket() 으로 생성된 핸들
  • level : 우선 SOL_SOCKET 만 알아두자 
  • optname : 설정 이름
  • optval : 설정 값
  • optlen : 설정 값 데이터의 크기 (e.g. sizeof(int))
설명:
  • 예제로 제일 많이 사용하는 옵션인 SO_REUSEADDR 을 설명
  • bind() 하려는 port 가 이미 다른 프로세스 등에서 bind() 했다면 해당 프로세스가 port 를 반환하기 전까지는 사용할 수 없다.
  • 때로는 port 반환이 시간이 걸리거나 프로세스가 비정상 종료되어 close() 를 제대로 해주지 못하는 경우가 있다.
  • 이런 경우 socket 에 SO_REUSEADDR 옵션을 주면 해당 port 가 실제로 소유주가 없는 상태라면 할당 받을 수 있도록 한다.
  • 이 외에도 여러 옵션들이 있으며 상황에 따라 구글에서 사용법 및 구동 방식을 참고하면 되겠다.

예)
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,  (void*)&reuse, sizeof(reuse)) != 0) {
    // error
}

getsockopt


int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); 

http://man7.org/linux/man-pages/man2/getsockopt.2.html

파라메터:
  • sockfd : socket() 으로 생성된 핸들
  • level : 우선 SOL_SOCKET 만 알아두자 
  • optname : 설정 이름
  • optval : 설정 값이 담길 데이터
  • optlen : 설정 값 데이터의 크기 (e.g. sizeof(int))
설명:
  • setsockopt() 과 반대로 현재 설정된 값을 가져온다.
  • Socket option 에는 set/get 모두 가능한 option 이 있고 set/get 중 하나만 가능한 option 도 있다.
  • 필요한 경우 구글에서 검색해 보도록 하자.

참고사항


Socket API 의 공통점이라면 socket() 과 accept() 함수를 제외하고 성공시 모두 0 을 return 한다.


Socket API 데이터 타입


  • struct sockaddr : 기본 주소 구조체
  • struct sockaddr_in : IPv4 주소 구조체
  • struct sockaddr_in6 : IPv6 주소 구조체
  • socklen_t : socket API 들이 사용하는 기본적인 크기 표시용 데이터 타입


IPv4 와 IPv6 대응


sockaddr 구조체는 이렇게 생겼다.

struct sockaddr
{
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
};

출처: http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/bits/socket.h.html


sockaddr_in 과 sockaddr_in6 는 이렇게 생겼다.

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};

/* Ditto, for IPv6.  */
struct sockaddr_in6
{
    __SOCKADDR_COMMON (sin6_);
    in_port_t sin6_port;        /* Transport layer port # */
    uint32_t sin6_flowinfo;     /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id;     /* IPv6 scope-id */
};

출처: http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/netinet/in.h.html

모두 __SOCKADDR_COMMON 라는 macro 로 시작한다.

__SOCKADDR_COMMON macro 는 이렇게 생겼다.


#define __SOCKADDR_COMMON(sa_prefix) \
    sa_family_t sa_prefix##family

http://repo-genesis3.cbi.utsa.edu/crossref/ns-sli/usr/include/netinet/in.h.html


sockaddr_in 와 sockaddr_in6 구조체의 처음 인자가 동일하게 sa_family_t 형태의 변수다.
family 멤버 변수는 AF_INET 또는 AF_INET6 가 입력되었던 부분이다.
sockaddr_in 또는 sockaddr_in6 의 첫번째 인자인 family 값이 AF_INET 또는 AF_INET6 인지 확인 후 type casting 을 이용해 다른 처리를 할 수 있다.

이후 예제를 통해 다시 살펴 보겠다.

Cross Platform


위 API 들은 Winsock API 에도 동일하게 존재함

Winsock API 에 다른 점
  • socket 변수 타입 : int 대신 SOCKET 을 사용
  • Socket API 에러 리턴 값 : socket() 이나 accept() 에러 리턴 값은 INVALID_SOCKET, 이외 API 함수들은 SOCKET_ERROR 를 리턴함
  • close() 대신 closesocket() 의 사용

2015년 12월 1일 화요일

C 언어 - 네트워크 프로그래밍

네트워크 프로그래밍


네트워크 프로그래밍이란 다른 프로세스 간 통신하는 프로그램을 제작하는 일을 말한다.
자신의 컴퓨터 내 다른 프로세스 간 통신 부터 국가 간 통신도 가능하다는 점이 매력적이다.

소켓


네트워크 연결은 소켓 단위로 관리된다.

IP 주소


컴퓨터의 네트워크 카드에 할당된 임시 주소이다.

IPv4 와 IPv6


IPv4 는 32bits 주소 체계를 갖는다.
수많은 PC, 모바일 등의 IP 기기들이 생겨나면서 IPv4 주소는 이미 고갈되었다.
IPv6 는 128bits 주소 체계를 갖어 IPv4 보다 훨씬 많은 주소를 할당할 수 있다.
주소 체계 이외에도 많은 부분이 개선되어 IPv4 에서 IPv6 로의 이동은 이미 진행 중이고 곧 생활로 다가올 것이다.

Port 번호


컴퓨터 한대로도 여러 연결이 가능한 이유는 port 번호로 여러 연결이 관리되기 때문이다.
소켓 하나당 하나의 port 를 할당 받는다.

서버/클라이언트


서버는 클라이언트의 접속을 기다리고 클라이언트와 대화한다.
클라이언트는 대기하고 있는 서버에 연결하고 대화한다.

대화와 프로토콜


사람이 전화를 걸 때를 연상해 보면 된다.
전화 거는 사람은 우선 상대의 전화 번호를 알고 전화를 건다.
일반적으로 전화 받은 상대가 먼저 '여보세요' 라고 하면 전화 건 사람이 '저는 누구누구입니다' 로 대화를 시작한다.
'여보세요' 라고 물어보면 누군지 이야기하는 대화 형식은 일종의 약속이다.
프로토콜은 이러한 약속을 의미한다.
컴퓨터 간의 대화는 사람 간의 대화보다 융통성이 없기 때문에 대화가 고급화 될 수록 프로토콜이 방대해 지는 면이 있다.

HTTP 프로토콜


웹 서버/웹 브라우저 은 HTTP 프로토콜을 구현한 프로그램이다.
HTTP 프로토콜은 범용이기 때문에 상세한 프로토콜이 공개되어 있다.
이 외 범용 프로토콜은 모두 공개되어 있다.
웹 서버나 웹 브라우저를 만들고 싶다면 HTTP 프로토콜을 참고해서 구현하면 시중의 어떠한 웹 서버나 웹 브라우저와도 통신할 수 있다.

TCP 와 UDP 연결


TCP 는 지속적이고 패킷 유실이 보완된 연결에 적합한 연결이고 UDP 는 단발성 데이터 전송에 적합한 연결이다.
TCP 연결은 서버/클라이언트 간 서로 대화를 주고 받을 필요가 있을 때 주로 사용한다.
UDP 연결은 주로 한 쪽에서 다른 쪽으로 데이터를 전달하는데 집중할 때 주로 사용한다.
플랫폼 측면에서 UDP 를 지원하는 것이 TCP 를 지원하는 것보다 쉬운 면이 있기 때문에 최소한의 성능이 요구되는 시스템에서 유일하게 지원하는 경우도 있다.
UDP 를 이용하면서 TCP 에서 제공하는 보완 기능들을 소프트웨어로 구현하는 경우도 많다.

패킷


음성이나 미디어 데이터들은 실제로는 작은 단위로 나뉘어져 상대방에게 전달된다.
이 작은 단위를 패킷이라고 부른다.
패킷은 단순히 데이터만 담고 있지는 않고 상대방의 주소 및 전송 방식에 따른 부수 정보들이 헤더에 포함된다.

헤더 + 데이터

구조는 프로토콜마다 달라지지만 대부분 비슷하다.

버퍼


네트워크로 전송되는 데이터 량이 디스크가 쓰는 속도보다 빠르게 유입된다면 데이터는 유실 될까?
네트워크로 전송되는 데이터들은 우선 캐시라는 빠른 메모리 영역에 잠시 저장되었다가 소프트웨어에서 읽어 가면 캐시 영역을 비우는 식의 버퍼가 마련되어 있다.
버퍼의 크기는 운영체제에서 관리하는 영역이기 때문에 버퍼 크기를 관리하는 API 를 운영체제에서 지원해줘야 소프트웨어로 제어할 수 있다.
대부분의 운영체제는 지원한다.
버퍼에 대한 개념은 YouTube 동영상 버퍼링하는 모습을 보면서도 익히 친숙한 개념일 것이다.

예제 코드 (리눅스)


tcpserver.c



tcpclient.c



테스트 방법


$ ./tcpserver &
$ ./tcpclient

예제 코드는 9000 번 포트를 사용하려고 하기 때문에 이미 다른 프로세스에서 9000 번을 사용하고 있다면 위 예제 프로그램은 실패한다.


2015년 9월 10일 목요일

C 언어 - linked list

지금까지 이야기한 내용을 토대로 linked list 를 구현해 보자.

Linked list


List 는 목록을 의미하고 linked 는 각 항목들이 연결되어 있다는 의미다.
특징으로는 프로그램 실행 중 동적으로 항목을 추가하고 제거하고 특정 항목에 접근이 가능하다.
배열은 선언시 크기가 정해지지만 linked list 는 미리 제약을 두지 않는다.
heap 메모리를 이용한다.

node


각 항목들은 node 라는 형태의 구조체에 담긴다.

typedef struct _node_t {
    int data;
    struct _node_t * next;
} node_t;


  • data : 사용자가 지정할 값
  • next : 또 다른 node_t 포인터


포인터와 heap 메모리에 대한 설명을 떠올려 보면 예상할 수 있겠지만
새로운 node_t 메모리를 할당하여 next 포인터에 대입시켜 list 에 항목을 추가해 나간다.

node 구조 설계


위에서 data 타입을 int 로 지정하였기 때문에 int 외에 다른 타입의 list 를 원한다면 node_t struct 부터 다시 작성하고 추가 삭제 함수를 다시 작성하여야 하는 문제가 있다.

data 타입을 특정 짓게 되면 node 추가/삭제/탐색 코드에 종속성이 생긴다.
data 타입이 바뀌면 줄줄이 수정해야 하는 코드가 발생하게 됨을 의미한다.

우리는 늘 이러한 종속성을 제거하려고 노력해야 한다.

이러한 종속성은 자바 ArrayList 를 사용해 보았다면 언어 자체에서 지원해 주는 편이 개발자에게는 편하다는 사실을 알 수 있다.

자바 ArrayList 는 꽤나 오랜 세월동안 꽤나 많은 개발자들에 의해 그 사용성이나 실용성에 대한 검증을 받았다.

C 언어의 void 포인터의 특성을 이용해서 이러한 종속성을 해결할 수 있다.

void * (void 포인터) 와 cast


void 는 함수의 파라메터나 리턴값이 없음을 의미했다.
void * 는 모든 타입 포인터를 대입할 수 있다.

void * data;
int num;
char ch;
data = (void*)&num;
data = (void*)&ch;

cast 는 포인터 타입 변환을 통한 데이터 접근 방법을 명시하는 기능이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _header_t {
    char magic[2];
    int length;
} header_t;
int main(int argc, char *args[]) {
    char data[1024] = {0,};
    {
        // write data
        header_t * header = (header_t*)data;
        header->magic[0] = 'M';
        header->magic[1] = 'Z';
        strcpy((char*)(data + sizeof(header_t)), "Hello World");
        header->length = strlen((char*)(data + sizeof(header_t))) + 1;
    }
    {
        // read data
        header_t * header = (header_t*)data;
        int length = header->length;
        char * text = (char*)malloc(length);
        strncpy(text, (char*)(data + sizeof(header_t)), length);
        printf("MAGIC: %c%c, LENGTH: %d, TEXT: %s\n", header->magic[0], header->magic[1], length, text);
    }
    return 0;
}

위 예제를 통해 데이터 뭉터기를 어떤 타입 포인터에 cast 하느냐에 따라 데이터 접근 방법이 달라진다는 특징을 알 수 있다.

이 말은 실제로 데이터 뭉터기이가 배열인지 문자열인지 개발자가 신경써야 한다는 의미다.

head node


시작 node 는 list 의 첫번째 node 를 가리키는 포인터이다.

node_t * head = NULL;

node 생성


static node_t * s_create_node(int data) {
    node_t * node = (node_t *)malloc(sizeof(node_t));
    memset(node, 0, sizeof(node_t));
    node->data = data;
    return node;
}

node 추가


static void s_add_node(node_t ** head, node_t * node) {
    node_t * temp = *head;
    if (!*head) {
        *head = node;
        return;
    }
    for (;temp && temp->next; temp = temp->next) {}
    temp->next = node;
}

node 탐색


tatic node_t * s_find(node_t * head, int data) {
    node_t * temp = head;
    for (; temp; temp = temp->next) {
if (temp->data == data) {
            return temp;
        }
    }
    return NULL;
}

node 제거


static void s_remove_node(node_t ** head, node_t * node) {
    node_t * temp = *head;
    if (*head == node) {
        *head = (*head)->next;
        return;
    }
    for (;temp && temp->next; temp = temp->next) {
        if (temp->next == node) {
            temp->next = node->next;
            free(node);
            break;
        }
    }
}

list.c


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _node_t {
    int data;
    struct _node_t * next;
} node_t;
static node_t * s_create_node(int data) {
    node_t * node = (node_t *)malloc(sizeof(node_t));
    memset(node, 0, sizeof(node_t));
    node->data = data;
    return node;
}
static void s_add_node(node_t ** head, node_t * node) {
    node_t * temp = *head;
    if (!*head) {
        *head = node;
        return;
    }
    for (;temp && temp->next; temp = temp->next) {}
    temp->next = node;
}
static void s_remove_node(node_t ** head, node_t * node) {
    node_t * temp = *head;
    if (*head == node) {
        *head = (*head)->next;
        return;
    }
    for (;temp && temp->next; temp = temp->next) {
        if (temp->next == node) {
            temp->next = node->next;
            free(node);
           break;
        }
    }
}
static void s_destroy(node_t ** head) {
    node_t * temp = *head;
    while(temp) {
        node_t * next = temp->next;
        free(temp);
        temp = next;
    }
    *head = NULL;
}
static int s_size(node_t * head) {
    int i = 0;
    node_t * temp = head;
    for (; temp; temp = temp->next, i++) {}
    return i;
}
static node_t * s_get(node_t * head, int index) {
    int i = 0;
    node_t * temp = head;
    for (; temp && i != index; temp = temp->next, i++) {}
    return temp;
}
static node_t * s_find(node_t * head, int data) {
    node_t * temp = head;
    for (; temp; temp = temp->next) {
if (temp->data == data) {
            return temp;
        }
    }
    return NULL;
}
static void s_print_list(node_t * list) {
    int i;
    int size = s_size(list);
    for (i = 0; i < size; i++) {
        node_t * node = s_get(list, i);
        int data = node->data;
        printf("%d ", data);
    }
    printf("\n");
}
int main(int argc, char *args[]) {
    node_t * list = NULL;
    int i;
    int size;
    for (i = 0; i < 10; i++) {
s_add_node(&list, s_create_node(i * 5));
    }
    s_print_list(list);
    s_remove_node(&list, s_get(list, 5));
    s_remove_node(&list, s_get(list, 2));
    s_print_list(list);
    s_destroy(&list);
    return 0;
}

만약 data 타입을 void * 형태로 한다면 data 에 대한 heap 메모리 할당 해제가 추가되어야 한다.


2015년 8월 27일 목요일

C 언어 - 함수 포인터

함수 포인터를 이용해 더욱 확장성 있는 프로그램을 만들 수 있다.

콜백 함수


함수도 주소가 있다.
함수 주소값을 다른 모듈에 넘겨주고 필요한 경우 호출하도록 할 수 있다.

callback.c


디버그 모드 일때만 로그를 출력하는 프로그램

#include <stdio.h>
#include <string.h>
typedef void (*prototype_callback_log)(const char * message);
static void s_callback_log_verbose(const char * message) {
    printf("LOG: %s\n", message);
}
static void s_callback_log_silent(const char * message) {
    // do nothing
}
static void s_run(prototype_callback_log log) {
    log("Program Begin");
    log("Program End");
}
int main(int argc, char * args[]){
    prototype_callback_log log;
    if (argc > 1 && !strcmp(args[1], "debug")) {
        printf(" == DEBUG MODE ==\n");
        log = s_callback_log_verbose;
    } else {
        printf(" == RELEASE MODE ==\n");
        log = s_callback_log_silent;
    }
    s_run(log);
    return 0;
}


실행:
$ ./callback
 == RELEASE MODE ==
$ ./callback debug
 == DEBUG MODE ==
LOG: Program Begin
LOG: Program End


Interface


위 예제의 함수 prototype 부분을 헤더 파일로 이동하여 프로젝트 전반에 걸쳐 공유할 수 있다.
여러 모듈간 데이터가 오가는 경우 미리 함수 prototype 을 지정하게 되면 각자 모듈을 구현하는데 고민할 부분이 한정되어 충돌없이 동시에 각자의 모듈을 구현할 수 있다.

Interface 라는 말은 함수 prototype 만을 지칭하는 용어는 아니고 미리 통신 방식이나 주고 받을 데이터 규약을 통칭한다.

Interface 를 정의할 때는 간략하고 필요한 데이터만 정의하도록 한다.
충분한 구현 경험이나 이해없이 좋은 interface 를 정하기는 어렵다.

결과물에 대한 그림이 머리속에 그려질 수 있어야 한다.

C 언어 - 파일 입출력

파일 다루는 방법을 알면 더욱 확장성 있는 프로그램을 만들 수 있다.

파일 처리 표준 API



  • fopen : 파일 열기
  • fread : 파일에서 읽기

size_t fread(void * ptr, size_t size, size_t count, FILE * stream);

    • ptr : 데이터를 읽어서 담을 변수 (최소 size * count 크기)
    • size : 한번에 읽어들일 크기
    • count : 읽어들일 횟수
    • stream : FILE 포인터

리턴값 : 읽어들인 크기, count 값과 다르다면 파일 끝에 도달 했거나 에러


  • fclose : FILE 포인터 닫기


dump.c


파일을 읽어서 화면에 출력

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *args[]) {
    char * filename = NULL;
    FILE * fp = NULL;
    char buffer[1024] = {0,};
    if (argc < 2) {
        exit(1);
    }
    filename = args[1];
    fp = fopen(filename, "r");
    while (fread(buffer, 1, sizeof(buffer) - 1, fp) > 0) {
        printf("%s", buffer);
        memset(buffer, 0, sizeof(buffer));
    }
    fclose(fp);
    return 0;
}

 conf.c

configuration 파일

  • 이름: 값 형태의 파일

configuration 파일 생성
configuration 파일 읽기

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static size_t s_strnspn_r(const char * str1, const char * str2, size_t size) {
    size_t len = strnlen(str1, size);
    size_t i, j;
    for (i = 0; i < len; i++) {
        char ch = str1[len - i - 1];
        for (j = 0; str2[j]; j++) {
            if (str2[j] == ch) {
                break;
            }
        }
        if (!str2[j]) {
            break;
        }
    }
    return len - i;
}
static void s_trim(const char * text, char * out, size_t size) {
    int s = strspn(text, " \t\n");
    int e= s_strnspn_r(text + s, " \t\n", strlen(text + s));
    int length = e;
    strncpy(out, text + s, (length > size ? size - 1 : length));
}
static void s_parse_conf(const char * text, char * name, int size_name, char * value, int size_value) {
    char name_buffer[1024] = {0,};
    size_t c = strcspn(text, ":");
    if (c == strlen(text)) { // not found ':'
s_trim(text, name, size_name);
        return;
    }
    strncpy(name_buffer, text, c);
    s_trim(name_buffer, name, size_name);
    s_trim(text + c + 1, value, size_value);
}
static void s_write_conf(FILE * fp, const char * name, const char * value) {
    char buffer[1024] = {0,};
    snprintf(buffer, sizeof(buffer), "%s : %s\n", name, value);
    fwrite(buffer, strlen(buffer), 1, fp);
}
static void s_test_write(const char * filename) {
    FILE * fp;
    fp = fopen(filename, "w");
    if (!fp) {
        perror("fopen()");
        exit(1);
    }
    s_write_conf(fp, "Name", "Steve Bolt");
    s_write_conf(fp, "Age", "26");
    fclose(fp);
}
static void s_test_read(const char * filename) {
    FILE * fp;
    char buffer[1024] = {0,};
    fp = fopen(filename, "r");
    if (!fp) {
        perror("fopen()");
        exit(1);
    }
   while (fgets(buffer, sizeof(buffer), fp)) {
char name[128] = {0,};
        char value[128] = {0,};
        s_parse_conf(buffer, name, sizeof(name), value, sizeof(value));
printf("Key : \"%s\", Value: \"%s\"\n", name, value);
    }
    fclose(fp);
}
int main(int argc, char *args[]) {
    if (argc < 2) {
        printf("Error: file name required\n");
        exit(1);
    }
    s_test_write(args[1]);
    s_test_read(args[1]);
    return 0;
}
실행:
$ ./conf person.txt
Key : "Name", Value: "Steve Bolt"
Key : "Age", Value: "26"


2015년 8월 26일 수요일

C 언어 - 문자열

문자열을 잘 처리할 줄 알게 되면 프로그래밍에 자신감이 붙게 된다.

문자열 처리를 위해서는 지금까지 이야기한 초기화와 배열 처리에 대한 이해가 있어야 한다.

문자열 초기화


고정 문자열

const char * name = "Hello";

"Hello" 는 자동으로 const char 배열로 선언된다.
const char * 는 문자열을 변형할 수 없다는 의미다.

배열 크기


문자열은 끝을 표시하기 위해 '\0' 이 추가로 붙는다.
H e l l o \0
문자열 Hello 는 5문자 이지만 자동으로 6 바이트 배열이 선언된다.

변경 가능 문자열


char name[128];
strcpy(name, "Steve");
printf("Name: %s\n", name);
strcpy(name, "Bob");
printf("Name: %s\n", name);

실행:
Name: Steve
Name: Bob

strcpy 분석


char * strcpy(char * destination, const char * source);
http://www.cplusplus.com/reference/cstring/strcpy/



  • char * destination : 복사 대상
  • const char * source : 복사할 문자열 (const char * 로 선언함으로써 strcpy 내부에서 source 는 변경되지 않음을 보장 받을 수 있다)

strcpy 소스
http://www.opensource.apple.com/source/Libc/Libc-167/gen.subproj/ppc.subproj/strcpy.c

char * strcpy(char *s1, const char *s2)
{
    char *s = s1;
    while ((*s++ = *s2++) != 0);
    return (s1);
}


  • (*s++ = *s2++) != 0 : 0, 즉 문자열 끝을 알리는 0 을 만날 때 까지 destination 에 한 문자씩 대입

결론적으로 source 는 반드시 0 으로 끝나야 한다.


strncpy

str n cpy

destination 이 source 보다 배열 크기가 작을 경우 어떻게 처리해야 할까?
destination 크기 까지만 복사하고 나머지는 버리는게 대부분 맞다.

char * strncpy ( char * destination, const char * source, size_t num );


  • destination : 복사 대상
  • source : 복사할 문자열
  • num : 최대 복사 크기


num 에는 보통 복사 대상의 크기 - 1를 입력한다.
- 1 은 마지막 0 을 위해 남겨둔다.

예)

char buffer[10] = {0,};
strncpy(buffer, "Donovan Harrison", sizeof(buffer) - 1);
printf("%s\n", buffer);


실행:
Donovan H

안전한 함수


n 자가 들어간 함수들은 버퍼 크기 안에서만 처리하게 되기 때문에 안전한 함수들이다.
안전한 함수들을 사용하자.


  • strncpy() : 문자열 복사
  • strnlen() : 문자열 길이
  • strncmp() : 문자열 비교
  • strncat() : 문자열 덧붙이기
  • snprintf() : 문자열 만들기


문자열 처리 표준 API 예


#include <stdio.h>
#include <string.h>
static void s_strncpy() {
    char buffer[10] = {0,};
    strncpy(buffer, "Donovan Harrison", sizeof(buffer) - 1);
    printf("%s\n", buffer);
}
static void s_strnlen() {
    char buffer[10] = {0,};
    size_t len;
    len = strnlen(buffer, sizeof(buffer));
    printf("buffer: \"%s\", length: %lu\n", buffer, len);
    strncpy(buffer, "hello", sizeof(buffer) - 1);
    len = strnlen(buffer, sizeof(buffer));
    printf("buffer: \"%s\", length: %lu\n", buffer, len);
}
static void s_strncmp() {
    char a[10] = {0,};
    char b[10] = {0,};
    strncpy(a, "abc", sizeof(a) - 1);
    strncpy(b, "abc", sizeof(b) - 1);
    printf("%s == %s : %d\n", a, b, strncmp(a, b, sizeof(a)));
    strncpy(a, "abc", sizeof(a) - 1);
    strncpy(b, "cba", sizeof(b) - 1);
    printf("%s == %s : %d\n", a, b, strncmp(a, b, sizeof(a)));
    strncpy(a, "abc", sizeof(a) - 1);
    strncpy(b, "a", sizeof(b) - 1);
    printf("%s == %s : %d\n", a, b, strncmp(a, b, sizeof(a)));
    strncpy(a, "a", sizeof(a) - 1);
    strncpy(b, "abc", sizeof(b) - 1);
    printf("%s == %s : %d\n", a, b, strncmp(a, b, sizeof(a)));
}
static void s_strncat() {
    char buffer[128] = {0,};
    strncpy(buffer, "Hello ", sizeof(buffer) - 1);
    strncat(buffer, "Steve", sizeof(buffer) - 1);
    printf("%s\n", buffer);
}
static void s_snprintf() {
    char buffer[128] = {0,};
    snprintf(buffer, sizeof(buffer), "Your name is %s and age is %d\n", "Bob", 24);
    printf("%s\n", buffer);
}
int main(int argc, char *args[]) {
    printf(" == strncpy == \n");
    s_strncpy();
    printf(" == strnlen == \n");
    s_strnlen();
    printf(" == strncmp == \n");
    s_strncmp();
    printf(" == strncat == \n");
    s_strncat();
    printf(" == snprintf == \n");
    s_snprintf();
    return 0;
}

실행:
 == strncpy ==
Donovan H
 == strnlen ==
buffer: "", length: 0
buffer: "hello", length: 5
 == strncmp ==
abc == abc : 0
abc == cba : -2
abc == a : 98
a == abc : -98
 == strncat ==
Hello Steve
 == snprintf ==
Your name is Bob and age is 24



2015년 8월 25일 화요일

C 언어 - 초기화

값의 초기화


C 언어는 개발자를 대신해서 값을 초기화 하지 않는다.

#include <stdio.h>
void foo() {
    int a;
    printf("%d\n", a);
}
int main() {
    int a;
    printf("%d\n", a);
    foo();
    return 0;
}

실행
0
32672

컴파일러 warning

 initialize.c: In function ‘foo’:
 initialize.c:10:8: warning: ‘a’ is used uninitialized in this function [-Wuninitialized]
   printf("%d\n", a);
         ^

초기화 하지 않고 변수를 사용하게 되면 의도치 않는 실행 결과를 가져올 수 있기 때문에 컴파일러 차원에서도 경고를 표시한다.

값 초기화 하기


일반 변수 및 배열 초기화


char * str = NULL;
int number = 0;
char array[64] = {0,};

struct 초기화


struct person_t {
    char name[128];
    int age;
};
struct person_t person = {{0,}, 0};

명시적 방법

struct peson_t person = {
    .name = {0,},
    .age = 0
};

heap 메모리 초기화


str = (char*)malloc(24);
memset(str, 0, 24);

memset


memset 은 메모리를 초기화하는 표준 API 이다. (stdlib.h)
http://www.cplusplus.com/reference/cstring/memset/

void * memset(void * ptr, int value, size_t num);

파라메터

  • ptr : 초기화하려는 메모리 포인터
  • value : 초기화 값
  • num : 메모리 크기

리턴 값

  • void * : ptr 값이 리턴됨



C 언어 - heap 메모리

메모리 구역


메모리 구역은 stack 과 heap 메모리 영역으로 나뉜다.

이제껏 stack 영역에 변수를 선언했다.
가장 큰 특징은 변수 선언만 하고 해제는 자동으로 되기 때문에 신경쓰지 않아도 된다.

heap 메모리 영역은 개발자가 메모리 할당과 해제를 신경써야 한다.

#include <stdio.h>
#include <stdlib.h>
int main() {
    char * p = (char*)malloc(5);
    char * a = p;
    *(a++) = 1;
    *(a++) = 2;
    *(a++) = 3;
    *(a++) = 4;
    *(a++) = 0;
    a = p;
    while (*a) {
        printf("%d\n", *a);
        a++;
    }
    free(p);
    return 0;
}

실행:
1
2
3
4

  • malloc() : 입력한 숫자 (바이트 단위) 만큼 heap 메모리를 할당한다.
  • free() : 할당한 메모리를 해제한다.

주의 사항
할당한 메모리는 사용이 끝나면 반드시 해제해 줘야 한다.
당연하게도 메모리 사용 한도가 존재하기 때문에 자칫 메모리 해제를 놓치게 된다면 과도한 메모리 사용으로 인해 성능이 저하되거나 프로세스가 멈출 수 있다.

성능 문제

힙 메모리를 할당하고 해제하는 작업은 생각보다 시간을 할애하는 작업이기 때문에 과도한 힙 메모리 할당/해제 작업은 고민을 해야 한다.


C 언어 - 포인터 (메모리 관리)

메모리

이미테이션 게임이라는 영화를 보니 튜링이라는 사람이 컴퓨터 모델을 처음으로 구현한 사람으로 소개되었다.

영화에는 소개되지 않았지만 따로 위키를 검색해 보니 튜링 기계의 무한히 긴 띠가 컴퓨터 메모리에 비유된다고 설명한다.

긴 띠의 종이 폭 만큼의 데이터 조각 하나 당 크기를 갖을 수 있다.
데이터 조각이 쓰인 위치를 주소라고 부른다.

주소  데이터
0x00 00000000
0x01 00000000
0x02 00000000
0x03 00000000
0x04 00000000
...


  • 데이터 조각 하나를 바이트라고 생각하자.
  • 데이터 조각 여러개를 모아 하나의 의미를 이룰 때 short, int, float, double 또는 struct 를 사용한다.
  • 여러벌의 데이터 조각은 아래와 같이 데이터가 순차적으로 밀집되어 있다.
int a => 0x7ffc9a38dca4 0x7ffc9a38dca5 0x7ffc9a38dca6 0x7ffc9a38dca7

    • int 는 4 바이트 값으로써 시작 주소값은 운영체제가 정하지만 시작 주소값을 기준으로 순차적으로 4 개의 주소 값을 갖는다.


선언된 변수의 주소 값은 영역의 끝 또는 프로그램 종료 시 소유권을 포기한다.

정리


  1. 어느 위치에 메모리가 선언될지는 운영체제가 정한다.
  2. 우리는 메모리가 밀집되어 선언된다는 특성을 기억할 필요가 있다.
  3. 그리고 C 언어로 주소값을 이용해 성능 좋은 프로그램을 만들 수 있다.

포인터


 주소값 확인


int value = 10;
printf("%p\n", &value);

실행:
0x7ffc8461ced4


  • %p : 포인터 값 출력
  • & : 변수 앞에 입력하면 주소값 반환


포인터 변수 선언 및 값 참조


int * ptr_value = NULL;
int value = 10;

ptr_value = &value;

printf("%p\n", ptr_value);
printf("%d\n", *ptr_value);

실행:
0x7fff52d16d94
10


  • int * : 변수 타입에 * 를 붙이면 포인터 변수 선언
  • NULL : 없음을 의미하며 실제 값은 0
  • * : 포인터 변수 앞에 사용하면 값을 참조


포인터 변수는 값을 저장하지 않고 주소 값만 저장한다.

주소값으로 작업하기

 

함수로 보내기


#include <stdio.h>

static void s_set_value(int value) {
    value = 10;
}

static void s_set_value_pointer(int * value) {
    *value = 10;
}


int main() {
    int val = 0;
    s_set_value(val);
    printf("
s_set_value() : %d\n", val);

    s_set_value_pointer(&val);
    printf("
s_set_value_pointer() : %d\n", val);

    return 0;
}

실행:
s_set_value() : 0s_set_value_pointer() : 10


함수 파라메터로 넘어간 값은 메모리 복사된다.

s_set_value():
  1. s_set_value() 함수로 입력한 val 파라메터는 s_set_value() 함수의 int value 로 복사된다.
  2. s_set_value() 함수를 빠져나오면서 int value 는 제거된다.
s_set_value_pointer():
  1. s_set_value_pointer() 함수로 입력한 val 의 주소값은 s_set_value_pointer() 함수의 int * value 로 복사된다.
  2. s_set_value_pointer() 함수 안에서 *value = 10 을 통해 해당 주소에 위치한 데이터 공간에 10을 대입 시킨다.
  3. s_set_value_pointer() 함수를 빠져나오면 int * value 는 제거된다.
  4. val 값은 10으로 변경되어 있다.


배열을 포인터로 탐색하기


#include <stdio.h>
static void s_traverse_array_with_pointer(char * array) {

    char * p = array;

    while (*p) {
        printf("%c\n", *p);
        p++;
    }
}

int main() {
    s_traverse_array_with_pointer("hello");
    return 0;
}

실행:
h
e
l
l
o

  • while (*p) ; p 가 가리키는 값이 0 이 아니면 while 문을 수행한다.
  • p++ : 주소값에 1 증가 시킨다. (데이터가 순차적으로 밀집된 점을 이용)
  • "hello" : 문자열은 char 배열 형태이다.
hello 는 5글자로 이루어져 있지만 문자열의 끝을 표시하기 위해 6 바이트의 char 배열로 선언된다.

h e l l o \0
0 1 2 3 4 5

\0 : 문자 표기시 0은 \0 로 표기한다.


오래된 예제 : swap


#include <stdio.h>

static void s_swap(int * a, int * b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int a = 10;
    int b = 20;
    s_swap(&a, &b);
    printf("a: %d, b: %d\n", a, b);

    return 0;
}

실행:
a: 20, b: 10

  • temp: *a = *b 를 통해 a 값이 변경되기 때문에 기존의 a 값을 저장한 temp 변수가 필요하다.

프로그램 작성 시 임시 변수를 활용해 문제 해결할 수 있는 경우가 많으니 참고하자.

2015년 8월 18일 화요일

C 언어 - 표준 API

API (Application Programming Interface)


API 란 특정 기능을 지원하는 함수나 자료구조들의 모음이다.
API 를 사용하는 입장에서는 헤더 파일과 라이브러리 파일들을 위치를 정하고 컴파일러에게 해당 파일들의 위치를 알려주면 미리 구현된 함수 및 자료구조를 사용할 수 있다.

표준 API 란 일반적으로 프로그램을 개발하는데 있어 기초적으로 필요로 하는 API 이다.
표준 API 는 컴파일러에게 헤더 파일과 라이브러리 파일 위치를 알려주지 않아도 컴파일러에 의해 자동으로 로드 된다.

stdio


stdio.h 에는 io (input/output) 에 관한 함수들이 제공된다.

printf() 함수는 이미 사용해 보았다.

printf 함수의 선언문은 이렇다.
int printf ( const char * format, ... );

... 부분은 지금까지 이야기 하지 않았지만 유효한 C 언어 문법이다.



표준 API 목록과 자세한 사용법은 아래 사이트에 아주 잘 정리 되어 있다.




문자열 출력, 입력, 문자열 처리, 파일 처리, 시간 처리 등의 API 들이 이미 표준으로 제공되고 있다.

표준 API 를 사용하는데 주저할 필요는 없다.
표준 API 로만 이뤄진 소스 코드는 어느 플랫폼에서건 컴파일되고 대부분 실행 결과도 같을 것이다.

표준 API 에 대해서는 사용법만 제대로 숙지하면 된다.

표준 라이브러리


C 언어 표준 API 들은 라이브러리 파일 형태로 저장이 되어 있다.

다만 컴파일러가 이러한 라이브러리 들을 기본적으로 로드하고 있기 때문에 개발자들이 신경쓰지 않아도 될 뿐이다.

표준 라이브러리를 찾아서

라이브러리 위치는 플랫폼마다 컴파일러마다 달라 진다.

리눅스 gcc 를 기준으로 설명하겠다.

$ gcc -xc -E -v -
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
Target: i686-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --disable-build-with-cxx --disable-build-poststage1-with-cxx --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch=i686 --build=i686-redhat-linux
Thread model: posix
gcc version 4.7.2 20120921 (Red Hat 4.7.2-2) (GCC)
COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=i686'
 /usr/libexec/gcc/i686-redhat-linux/4.7.2/cc1 -E -quiet -v - -mtune=generic -march=i686
ignoring nonexistent directory "/usr/lib/gcc/i686-redhat-linux/4.7.2/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/i686-redhat-linux/4.7.2/../../../../i686-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/i686-redhat-linux/4.7.2/include
 /usr/local/include
 /usr/include

End of search list.


$ gcc -xc -E -v - 명령을 통해 표준 라이브러리 위치를 확인할 수 있다.

  • /usr/lib/gcc/i686-redhat-linux/4.7.2/ 에는 libgcc.a 나 libstdc++.a 등의 .a 또는 .so  확장자를 갖는 라이브러리 파일들이 있다.
  • /usr/lib/gcc/i686-redhat-linux/4.7.2/include 에는 헤더 파일들이 있다.
gcc 컴파일러 옵션을 이용해 라이브러리 파일을 컴파일 할 수 있고 .so 또는 .a 형태의 라이브러리 파일과 헤더 파일을 배포하여 다른 프로젝트에서 사용하도록 할 수 있다.

동적 라이브러리와 정적 라이브러리


동적 라이브러리는 .so 확장자를 갖고 정적 라이브러리는 .a 확장자를 갖는다.
둘의 가장 큰 차이는 프로그램 실행에 있다.

  • 동적 라이브러리는 프로그램 실행시에도 필요하다.
  • 정적 라이브러리는 컴파일 단계에서 프로그램에 녹아들기 때문에 실행시 필요 없어진다.

libmyutil.so 라는 동적 라이브러리를 필요로 하는 myprogram 실행 파일을 컴파일 했다면 myprogram 실행시에도 libmyutil.so 파일이 필요하다.

만약 libmyutil.a 라는 정적 라이브러리를 필요로 하는 myprogram 실행 파일을 컴파일했다면 이미 myprogram 에 .a 내용이 포함되었기 때문에 실행시에는 libmyutil.a 파일이 더 이상 필요없다.

unix/linux 라이브러리 파일명 규칙


lib 로 시작하고 .so 확장자를 갖는다.
컴파일 옵션으로 입력할 때는 lib 와 .so 부분은 제거한다.

예) libmyutil.so 라이브러리 파일이 있다면
$ gcc -o hello main.c -lmyutil

-l 은 라이브러리 파일을 로드하는 옵션이다.

2015년 8월 17일 월요일

C 언어 - 계산

2진수


  • 10진수는 0 1 2 3 4 5 6 7 8 9 로 숫자를 표현한다.
  • 8진수는 0 1 2 3 4 5 6 7 로 숫자를 표현한다.
  • 16진수는 0 1 2 3 4 5 6 7 8 9 a b c d e f 로 숫자를 표현한다.
  • 2진수는 0 1 로 숫자를 표현한다.


2진수는 가짓수가 0 과 1 뿐이다.

컴퓨터 기억 소자 (1 비트) 는 전기적 상태를 기준으로 0 과 1 로 구분된다.

기억 소자 8개가 보여 1바이트를 이룬다.

char 형이 1 바이트 이다.

1바이트로 표현할 수 있는 가지수는 2의 8제곱 = 256 가지 이다.

2진수로 16 진수 0 ~ f 까지 표현하면 이렇다.
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
a 1010
b 1011
c 1100
d 1101
e 1110
f 1111

2진수 와 16진수를 오가는게 10진수와 변환하는 거 보다 잘 맞는다.


사칙 연산


더하기, 빼기, 나누기, 곱하기

printf("%d\n", 1 + 2);
printf("%d\n", 1 - 2);
printf("%d\n", 1 / 2);
printf("%d\n", 1 * 2);

실행:

3
-1
0
2

division by zero

0 으로 나누면 런타임 에러가 발생한다.
0 을 나누는 거는 허용된다.


  • 2 / 0 은 런타임 에러
  • 0 / 2 는 문제 없음 (결과는 0)


소수점 계산


float 나 double 형을 사용한다.

float a = 0.1;
float b = 0.2;
float sum = a + b;
printf("%f\n", sum);

실행:
0.300000

printf 문에서는 %f 를 사용한다.


소수점 절삭


float 형을 int 에 대입하면 어떤 일이 일어날까?

printf("%d\n", (int)0.1);
printf("%d\n", (int)0.2);
printf("%d\n", (int)0.4);
printf("%d\n", (int)0.5);
printf("%d\n", (int)0.6);
printf("%d\n", (int)0.9);
printf("%d\n", (int)1.0);

실행:
0
0
0
0
0
0
1

나머지 계산


printf("%d\n", 10 % 3);

실행:
1

비트 연산


OR 연산과 AND 연산, NOT

OR 연산 : |


  • printf("%d\n", 0 | 0); // 0
  • printf("%d\n", 0 | 1); // 1
  • printf("%d\n", 1 | 0); // 1
  • printf("%d\n", 1 | 1); // 1


AND 연산 : &


  • printf("%d\n", 0 & 0); // 0
  • printf("%d\n", 0 & 1); // 0
  • printf("%d\n", 1 & 0); // 0
  • printf("%d\n", 1 & 1); // 1


NOT


  • printf("%x\n", !0xff); // 0
  • printf("%x\n", !0x00); // 1


shift 연산


원하는 만큼 비트를 이동한다.

printf("%x\n", 0xff >> 1);
printf("%x\n", 0xff << 1);

실행:
7f
1fe
7f 는 2진수로 표기하면
0111 1111

1fe 는 2진수로 표기하면
0001 1111 1110

반전


printf("%x\n", ~0xff);
printf("%x\n", ~0x00);

실행:
ffffff00
ffffffff


flag 예제


비트 연산을 이용해 flag 값으로 사용하기도 한다.

#define READ 0x01
#define WRIT 0x02
#define EXEC 0x04
#define SPEC 0x08

int permission = 0x0f;

if (permission & READ) {
    printf("read permission\n");
}
if (permission & WRIT) {
    printf("write permission\n");
}
if (permission & EXEC) {
    printf("execute permission\n");
}
if (permission & SPEC) {
    printf("special permission\n");
}

비트를 활용하면 메모리를 절약하면서 여러 상태를 기억할 수 있다.

2015년 8월 16일 일요일

C 언어 - 자세히 알아보기

C 언어에 대해서 가장 잘 이해하고 있는 사람은 누굴까?

아마 컴파일러를 개발하는 사람들일 것이다.

컴파일러를 만드는 사람들은 개발자들이 컴파일러를 어떻게 사용하고 어떻게 프로그램을 개발해야하는지 등 많은 정보를 전파하기 위해 이를 문서로 남긴다.

개발자는 단순히 프로그램을 만드는 데 그치는 것이 아니라 다른 사람들이 잘 사용할 수 있도록 공유하고 전파하는 일종의 책임이 있다.

훌륭한 개발자가 되려면 이 사람들 처럼 되려고 노력하면 된다.

gcc 5.2.0 버전 문서

지금까지 설명한 키워드들을 검색해 보고 링크를 클릭하면 컴파일러를 만든 사람의 입장에서 아주 자세히 설명한 내용들이 있다.

항상 애매모호한 부분이 있다면 원작자가 기술한 문서가 있는지 확인해 보자

C 언어 - 전처리기, typedef, enum

전처리기 (preprocessor)



컴파일러는 전처리문을 읽고 해석하고 처리하여 코드로 변환한 뒤 원래 전처리문은 코드에서 제거한다.

예)
main.c

#define HELLO printf("hello\n")

int main() {
    HELLO;
}

전처리 후


int main() {
    printf("hello\n");
}

  • 위 과정은 컴파일러가 해석하는 부분이기 때문에 정확히 위와 같이 코드가 생성되지는 않겠지만
  • 컴파일 결과물은 전처리 후 코드와 같은 동작을 한다.

아래와 같은 전처리문들이 있다.

  • #include
  • #if
  • #else
  • #elif
  • #endif
  • #define
  • #ifndef
  • #ifdef
  • #error
  • #warning

특징을 보면 # 으로 시작한다는 점을 알 수 있다.

  • #include : 이미 보았겠지만 header 파일을 포함시킨다.
  • #if 조건문 : 분기문
  • #else : #if 조건문이 거짓인 경우
  • #endif : #if 문의 끝
  • #define 정의값 대체문 : 사용자 값 정의
  • #ifndef 정의값 : 정의값이 존재하는지 않는지 판단
  • #ifdef 정의값 : 정의값이 존재하는지 판단
  • #error 경고 문구 : 컴파일 에러 유발 (컴파일 시 경고 문구 출력)
  • #warning 경고 문구 : 컴파일 중 경고 문구 출력

예)

#ifdef __unix__
#    inculde <unistd.h>
#elif define _WIN32
#    include <Windows.h>
#endif

#include <stdio.h>
#include <stdlib.h>

#define LOG printf

int main() {
    LOG("Hello World\n");
}

위 코드는 unix 플랫폼일 때와 windows 플랫폼일 때 다르게 변화한다.

unix 플랫폼

#inculde <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Hello World\n");
}

windows 플랫폼

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Hello World\n");
}

unistd.h 헤더는 unix 계열 플랫폼에서만 참조할 수 있고 Windows.h 는 windows 계열 플랫폼에서만 참조할 수 있다.

__unix__ 나 _WIN32 등의 define 값은 컴파일러에 의해 미리 정의 된 값이다.

typedef


변수 타입을 추가한다.

문법:
typedef <유효한 C 타입> <새로운 타입명>;

반드시 유효한 C 타입만 typedef 처리할 수 있다.

예)
typedef int mynumber;

...

mynumber number = 10;

mynumber 는 컴파일 과정에서 int 로 대체된다.

예)
  • typedef char mycharacter;
  • typedef char* mystring;
  • typedef unsigned char uint8;
  • typedef unsigned int uint32;

char, short, int 라는 의미보다 데이터 크기가 중요한 경우 type 크기를 이름에 붙이기도 한다.

  • typedef unsigned char myuint8;
  • typedef unsigned short myuint16;
  • typedef unsigned int myuint32;

구조체 변수 선언문을 다시 보면 이렇다.

struct my_struct_t var;

typedef 로 지정하면 아래와 같이 사용할 수 있다.


typedef struct my_struct_t my_struct;

my_struct var;

enum



여러 상수를 열거하는 식으로 선언한다.

예)

enum my_enum {
    left,
    right,
    down,
    up
};

  • 위 문을 실행하면 left, right, down, up 이라는 상수가 자동으로 생성된다.
  • 자동으로 left = 0, right = 1, down = 2, up = 3 값이 지정된다.

마치 이런 코드와 같은 처리를 한다.
const int left = 0;
const int right = 1;
const int down = 2;
const int up = 3;

enum 변수 선언:

enum my_enum var;

enum 변수 참조:

var = left;

enum 변수 비교문:

switch (var) {
case left;
    break;
case right:
    break;
case down:
    break;
case up:
    break;
}

enum 상수 초기값 변경

기본으로 0 1 2 3 ... 순으로 지정이 되지만 초기값을 변경할 수 있다.

예)

enum direction {
    north = 1,
    south,
    west,
    east
};

north 값부터 1씩 더해 간다.
north = 1 ,south = 2, west = 3, east = 4 가 된다.

enum direction {
    north,
    south = 5,
    west,
    east
};

north = 0 ,south = 5, west = 6, east = 7 가 된다.

Cross platform 대응


전처리문 예를 보면 unix 에서 사용할 수 있는 헤더 파일과 windows 에서 사용할 수 있는 헤더 파일이 다르다.
뿐만 아니라. 지원하는 함수들도 다르다.
하지만 일맥상통하는 부분은 같거나 비슷한 기능을 제공하고 있다는 점이다.
다만 함수 이름이나 사용법이 다를 뿐이다.
이런 경우 전처리기와 typedef 문을 잘 활용하면 하나의 소스 코드로 여러 플랫폼에 컴파일 했을 때 문제가 없게 작성할 수 있다.
이러한 처리가 되어 소스 코드 하나로 여러 플랫폼에서 컴파일 할 수 있는 코드를 cross platform 한 코드라고 한다.