Socketの中継を考える(C言語編)


メールやらHTTP等の通信プログラムのプロトコルは大抵Scoketを使用して通信している。HTTPプロトコルが実際がどういうデータの流れになっているか知りたかったのだが、Socketを中継してみてそのデータを標準出力するプログラムを書いてみた。プロキシサーバのような外部Hostへの中継は難しいが、ローカルで動作しているサーバはポート番号だけ入れ替えることで出来るようになる。

通常は
ブラウザ ←→ HTTPサーバ(ポート80)
となっているのを
ブラウザ ←→ SocketRelayサーバ(ポート8080)←→ HTTPサーバ(ポート80)
のように中継させる事によって、ブラウザからはSocketRelayサーバがHTTPサーバとして動作しているように見えるようになる。SocketRelayサーバは単純にSocketの入出力ストリームを中継しているだけで実質何もしていない。HTTPだけではなくてPOPやSMTPやFTPなんかも同様に出来る。

ソースコード

//SocketRelay.c
#include <stdio.h>
#include <sys/types.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <netdb.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUFFER_SIZE ( 1024 * 16  ) // 16Kバッファ

static int serverSock;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//system signalのhandler
void signalHandler( int signal ){
    shutdown(serverSock, 2);
    fprintf(stderr,"EXIT SIGNAL:%d\n",signal);
    exit(EXIT_FAILURE);
}

//ファイル記述子を使って入力を出力に渡す
void *readToWrite(int file[]){
    char buffer[BUFFER_SIZE];
    ssize_t read_size;
    int in_file = file[0];
    int out_file = file[1];
    
    while(1){
        // 入力ストリームから読み込み
        read_size = read(in_file, buffer, sizeof(buffer));
        if (read_size == 0){
            break; //EOF
        }
        //close
        if(read_size < 0){
            break;
        }
        pthread_mutex_lock( &mutex );
        // 標準出力ストリームへ書き出し
        write(1,buffer,(unsigned int) read_size);
        pthread_mutex_unlock( &mutex );
        // 出力側ストリームへ書き出し
        write(out_file,buffer,(unsigned int) read_size);
    }
    return (file);
}

// サーバソケットを作成する
int createServerSoket(int port){
    int soc;
    char name[MAXHOSTNAMELEN];
    struct sockaddr_in socin;
    struct hostent *servhost;
    
    bzero(&socin, sizeof(struct sockaddr_in));
    gethostname(name, MAXHOSTNAMELEN);
    servhost=gethostbyname(name);
    
    if(servhost==NULL){
        fprintf(stderr, "ERROR: gethostbyname\n");
        exit(EXIT_FAILURE);
    }
    
    socin.sin_family = servhost->h_addrtype;
    socin.sin_port = htons(port);
    if((soc=socket(AF_INET,SOCK_STREAM,0))<0){
        fprintf(stderr, "ERROR: create socket\n");
        exit(EXIT_FAILURE);
    }
    if(bind(soc,(struct sockaddr *)&socin,sizeof(socin))<0){
        close(soc);
        fprintf(stderr, "ERROR: socket bind fail\n");
        exit(EXIT_FAILURE);
    }
    
    listen(soc,SOMAXCONN);
    return (soc);
}

//クライアントソケットを作成する
int createClientSoket(char *host, int port){
        int soc; 
        struct hostent *servhost;
        struct sockaddr_in server;
    
        servhost = gethostbyname(host);
        if ( servhost == NULL ){
            fprintf(stderr, "ERROR:gethostbyname(%s)\n", host);
            exit(EXIT_FAILURE);
        }
	    bzero(&server, sizeof(server));
        server.sin_family = AF_INET;
        bcopy(servhost->h_addr, &server.sin_addr, servhost->h_length);
    	server.sin_port = htons(port);
        if ( ( soc = socket(AF_INET, SOCK_STREAM, 0) ) < 0 ){
            fprintf(stderr, "ERROR: create socket\n");
            exit(EXIT_FAILURE);
        }
        if ( connect(soc, (struct sockaddr *)&server, sizeof(server)) == -1 ){
            fprintf(stderr, "ERROR: connect socket\n");
            exit(EXIT_FAILURE);
        }

    return (soc);
}

void *socketRelay(int file[]){
    int connectedSocket = file[0];
    int clientSock = file[1];
    
    int files_in[2] = {connectedSocket,clientSock};
    pthread_t	thread_id1, thread_id2;
    int			status;
    
    status=pthread_create(&thread_id1,NULL,(void *(*)(void *))readToWrite, files_in);
    if(status!=0){
        fprintf(stderr,"pthread_create : %s",strerror(status));
        exit(EXIT_FAILURE);
    }
    
    int files_out[2] = {clientSock,connectedSocket};
    status=pthread_create(&thread_id2,NULL,(void *(*)(void *))readToWrite, files_out);
    if(status!=0){
        fprintf(stderr,"pthread_create : %s",strerror(status));
        exit(EXIT_FAILURE);
    }
    
    pthread_join(thread_id2,NULL);
    pthread_cancel(thread_id1);
    
    close(connectedSocket);
    close(clientSock);
    
    return file;
}

void start(char *host, int serverPort, int clientPort){
    int connectedSocket;
    socklen_t len;
    pthread_t	thread_id1;
    int			status;
    
    serverSock = createServerSoket(serverPort);
    
    while(1){
        struct sockaddr_in peer_sin;
        
        len = sizeof(peer_sin);
        connectedSocket = accept(serverSock, (struct sockaddr *)&peer_sin, &len);
        if ( connectedSocket == -1 ){
            fprintf(stderr, "ERROR:accept socket\n");
            exit(EXIT_FAILURE);
        }
        int clientSock = createClientSoket(host,clientPort);
        int files[2] = {connectedSocket,clientSock};
        
        status=pthread_create(&thread_id1,NULL,(void *(*)(void *))socketRelay, files);
        if(status!=0){
            fprintf(stderr,"pthread_create : %s",strerror(status));
            exit(EXIT_FAILURE);
        }
    };
}

int main(int argc, char * argv[])
{

    if (argc < 4) {
        printf("Invalid arguments");
        return EXIT_FAILURE;
    }
    char *host = argv[1];
    struct hostent *hn;
    hn = gethostbyname(host);
    if(hn == NULL){
        printf("Invalid hostname");
        return EXIT_FAILURE;
    }
    
    int i=2;
    for (; i<4; i++) {
        char *port = argv[i];
        while(*port) {
            if(*port >= '0' && *port <= '9') port++;
            else break;
        }
        if (*port) {
            printf("Invalid port number:%s", argv[i]);
            return EXIT_FAILURE;
        }
    }
    
	signal( SIGINT, signalHandler );
	signal( SIGKILL, signalHandler );
	signal( SIGTERM, signalHandler );
    
    start(host, atoi(argv[2]), atoi(argv[3]));
    
    return EXIT_SUCCESS;
}

ネットワークやスレッドが絡むといろいろ知らない事が多かったので面倒だった。他の言語でも似たような感じになると思う。

使い方

$gcc -o socket-relay SocketRelay.c
$./socket-relay localhost 8080 80
ブラウザ等でhttp://localhost:8080/へアクセス
ctrl+cで終了

出力例

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31
Accept-Encoding: gzip,deflate,sdch
Accept-Language: ja,en-US;q=0.8,en;q=0.6
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.3

HTTP/1.1 200 OK
Date: Tue, 16 Apr 2013 14:59:19 GMT
Server: Apache/2.2.22 (Unix) DAV/2 PHP/5.3.15 with Suhosin-Patch mod_ssl/2.2.22 OpenSSL/0.9.8r
Content-Location: index.html.en
Vary: negotiate
TCN: choice
Last-Modified: Mon, 11 Feb 2013 08:55:12 GMT
ETag: "35d48-2c-4d56f111c5c00"
Accept-Ranges: bytes
Content-Length: 44
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html
Content-Language: en

<html><body><h1>It works!</h1></body></html>GET /favicon.ico HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31
Accept-Encoding: gzip,deflate,sdch
Accept-Language: ja,en-US;q=0.8,en;q=0.6
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.3

HTTP/1.1 404 Not Found
Date: Tue, 16 Apr 2013 14:59:20 GMT
Server: Apache/2.2.22 (Unix) DAV/2 PHP/5.3.15 with Suhosin-Patch mod_ssl/2.2.22 OpenSSL/0.9.8r
Content-Length: 209
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /favicon.ico was not found on this server.</p>
</body></html>
^CEXIT SIGNAL:2