a C HTTP Client

C#やJavaでは簡単にhttp Clientプログラムができる。C言語ではソケットを作成するところから必要です。またWindowsとLinux両方対応するように書いたのでプログラムが長くなる。

(bcc32で動作確認済み)

プログラムは、最初にsocketを生成してWebサーバに接続し、テキスト形式でHTTPリクエストメッセージを作成してWebサーバに送信する。
HTTPリクエストメッセージは、複数行から成り立つ一連のデータ列。ここでいう1行とは、終端にCR(キャリッジリターン、16進の0x0d)とLF(ラインフィード、16進の0x0a)を持つデータの単位を示す。ほぼ、通常のテキスト・データの1行と等しくなる。メッセージ・ヘッダとメッセージボディ部に分かれ、両者は空行(単独のCR+LF)で分割される。

httpを理解するには、telnetで手入力でHTTPをしゃべってみるがいい方法。
https://www.softel.co.jp/blogs/tech/archives/263

まずプログラムをリストする:

#include <stdio.h> /* printf, sprintf */
#include <stdlib.h> /* exit, atoi, malloc, free */
// #include <unistd.h> /* read, write, close */
#include <string.h> /* memcpy, memset */
#ifdef __linux__
    #include <sys/socket.h> /* socket, connect */
    #include <netdb.h> /* struct hostent, gethostbyname */
    #include <netinet/in.h> /* struct sockaddr_in, struct sockaddr */
#elif _WIN32
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <windows.h>
    #pragma comment(lib,"ws2_32.lib") //Winsock Library

#else

#endif

struct hostent *server;
struct sockaddr_in serv_addr;
int bytes, sent, received;
char response[4096];
int portno;
char *host;
char *path;

void error(const char *msg) { perror(msg); exit(0); }


#ifdef _WIN32

win_send_http(char *message){
  WSADATA wsa;
  SOCKET s;

  printf("\nInitialising Winsock...");
  if (WSAStartup(MAKEWORD(2,2),&wsa) != 0)
  {
      printf("Failed. Error Code : %d",WSAGetLastError());
      return 1;
  }

  printf("Initialised.\n");

  //Create a socket
  if((s = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET)
  {
      printf("Could not create socket : %d" , WSAGetLastError());
  }

  printf("Socket created.\n");

  server = gethostbyname(host);
  memset(&serv_addr,0,sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(portno);
  memcpy(&serv_addr.sin_addr.s_addr,server->h_addr,server->h_length);
  //Connect to remote server
  if (connect(s , (struct sockaddr *)&serv_addr , sizeof(serv_addr)) < 0)
  {
      printf("connect failed with error code : %d" , WSAGetLastError());
      return 1;
  }

  puts("Connected");
  if( send(s , message , strlen(message) , 0) < 0)
  {
      printf("Send failed with error code : %d" , WSAGetLastError());
      return 1;
  }
  puts("Data Send\n");

  //Receive a reply from the server
  if((received = recv(s , response , 2000 , 0)) == SOCKET_ERROR)
  {
      printf("recv failed with error code : %d" , WSAGetLastError());
  }

  puts("Reply received\n");

  //Add a NULL terminating character to make it a proper string before printing
  response[received] = '\0';
  puts(response);

  closesocket(s);
  WSACleanup();

  return 0;   // everything OK
}

#endif
#ifdef __linux__

linx_send_http(message) {
  int sockfd;
  int total;

  total = strlen(message);
  /* lookup the ip address */

  server = gethostbyname(host);
  if (server == NULL) error("ERROR, no such host");
      /* create the socket */
      sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0) error("ERROR opening socket");
      /* fill in the structure */
      memset(&serv_addr,0,sizeof(serv_addr));
      serv_addr.sin_family = AF_INET;
      serv_addr.sin_port = htons(portno);
      memcpy(&serv_addr.sin_addr.s_addr,server->h_addr,server->h_length);
              /* connect the socket */
      if (connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
          error("ERROR connecting");
              /* send the request */

  sent = 0;
  do {
      bytes = write(sockfd,message+sent,total-sent);
      if (bytes < 0)
          error("ERROR writing message to socket");
      if (bytes == 0)
          break;
      sent+=bytes;
  } while (sent < total);
  /* receive the response */
  memset(response, 0, sizeof(response));
  total = sizeof(response)-1;
  received = 0;
  printf("Response: \n");
  do {
     printf("%s", response);
     memset(response, 0, sizeof(response));
     bytes = recv(sockfd, response, 1024, 0);
      if (bytes < 0)
         printf("ERROR reading response from socket");
     if (bytes == 0)
         break;
     received+=bytes;
  } while (1);

  if (received == total)
      error("ERROR storing complete response from socket");

  /* close the socket */
  close(sockfd);

  return 0;   // everything OK
}

#endif

int main(int argc,char *argv[])
{
    int i;
    char message[4096];
    portno = atoi(argv[2])>0?atoi(argv[2]):80;
    host = strlen(argv[1])>0?argv[1]:"localhost";
    path = strlen(argv[4])>0?argv[4]:"/";

    if (argc < 5) { puts("Parameters: <host> <port> <method> <path> [<data> [<headers>]]"); exit(0); }

    /* How big is the message? */
    if(!strcmp(argv[3],"GET"))
    {
      if(argc>5)
          sprintf(message,"%s %s%s%s HTTP/1.0\r\nHost: %s\r\n",
              strlen(argv[3])>0?argv[3]:"GET",               /* method         */
              path,                                          /* path           */
              strlen(argv[5])>0?"?":"",                      /* ?              */
              strlen(argv[5])>0?argv[5]:"",host);            /* query string   */
      else
          sprintf(message,"%s %s HTTP/1.0\r\nHost: %s\r\n",
              strlen(argv[3])>0?argv[3]:"GET",               /* method         */
              path,host);                                    /* path           */
      for(i=6;i<argc;i++)                                    /* headers        */
          {strcat(message,argv[i]);strcat(message,"\r\n");}
      strcat(message,"\r\n");                                /* blank line     */
    }
    else
    {
        sprintf(message,"%s %s HTTP/1.0\r\nHost: %s\r\n",
            strlen(argv[3])>0?argv[3]:"POST",                  /* method         */
            path,host);                                        /* path           */
        for(i=6;i<argc;i++)                                    /* headers        */
            {strcat(message,argv[i]);strcat(message,"\r\n");}
        if(argc>5) {
          sprintf(message+strlen(message),"Content-Length: %d\r\n",(int)strlen(argv[5]));
          sprintf(message+strlen(message),"Content-Type: application/x-www-form-urlencoded\r\n");
        }

        strcat(message,"\r\n");                                /* blank line     */
        if(argc>5)
            strcat(message,argv[5]);                           /* body           */

              sprintf(message,"%s %s HTTP/1.0\r\nHost: %s\r\n",
                  strlen(argv[3])>0?argv[3]:"POST",                  /* method         */
                  path,host);                                        /* path           */
              for(i=6;i<argc;i++)                                    /* headers        */
                  {strcat(message,argv[i]);strcat(message,"\r\n");}
              if(argc>5) {
                sprintf(message+strlen(message),"Content-Length: %d\r\n",(int)strlen(argv[5]));
                sprintf(message+strlen(message),"Content-Type: application/x-www-form-urlencoded\r\n");
              }

              strcat(message,"\r\n");                                /* blank line     */
              if(argc>5)
                  strcat(message,argv[5]);                           /* body           */
    }

    printf("Processed\n");
    /* What are we going to send? */
    printf("Request:\n%s\n",message);

    #ifdef _WIN32

    win_send_http(message);

    #endif
    #ifdef __linux__

    linx_send_http(message);

    #endif

    return 0;
}

 

 

 

このプログラムを利用して、例のTinyWebDBを読み書きしてみる。

まずPostで、データを書き込む

C:\Users\chen\Documents\C\http-client>http-client-3 tinydb.work 80 POST /api/storeavalue/ “tag=presentationtimer&value=Just a TEST from C3”

Process 2
Allocating...
Processed
Request:
POST /api/storeavalue/ HTTP/1.0
Host: tinydb.work
Content-Length: 47
Content-Type: application/x-www-form-urlencoded

tag=presentationtimer&value=Just a TEST from C3

Initialising Winsock...Initialised.
Socket created.
Connected
Data Send

Reply received

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 12 Dec 2017 06:56:12 GMT
Content-Type: application/json
Content-Length: 52
Connection: close
X-Powered-By: PHP/5.6.31
Vary: Cookie
Set-Cookie: wfvt_2029330401=5a2f7d8c36e15; expires=Tue, 12-Dec-2017 07:26:12 GMT; Max-Age=1800; path=/; httponly
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-cache, must-revalidate

["STORED","presentationtimer","Just a TEST from C3"]

 

次は、Getで、書き込んだデータを取得

C:\Users\chen\Documents\C\http-client>http-client-3 tinydb.work 80 POST /api/getvalue/ “tag=presentationtimer”

Process 2
Allocating...
Processed
Request:
POST /api/getvalue/ HTTP/1.0
Host: tinydb.work
Content-Length: 21
Content-Type: application/x-www-form-urlencoded

tag=presentationtimer

Initialising Winsock...Initialised.
Socket created.
Connected
Data Send

Reply received

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 12 Dec 2017 06:57:10 GMT
Content-Type: application/json
Content-Length: 51
Connection: close
X-Powered-By: PHP/5.6.31
Vary: Cookie
Set-Cookie: wfvt_2029330401=5a2f7dc61d61c; expires=Tue, 12-Dec-2017 07:27:10 GMT; Max-Age=1800; path=/; httponly
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-cache, must-revalidate

["VALUE","presentationtimer","Just a TEST from C3"]

 

Dump file

DUMP で出力された内容を ダンプリスト と呼ぶ

ダンプリスト左端はアドレス(ファイル先頭からの位置)

ダンプリスト中央にある16進数(バイト)が列挙されてる部分がマシン語プログラム(バイトコード)を表している。

ダンプリスト右端は、バイトコードをキャラクタコードで表現したときの内容。ただしバイトで表現可能な数値はキャラクタコードの範疇を超えることがあるため、そのような場合はピリオドで表現される。

dump.c

#include <stdio.h>
int main(int argc, char* argv[])
{
    FILE *fp;
    unsigned char buf[16];   /* 読み込みバッファ */
    unsigned long addr = 0;  /* 先頭からのアドレス */
    int readnum, i;

    if(argc <= 1) {
        printf("usage:dump filename\n");
        return 1;
    }
    if(!(fp = fopen(argv[1], "rb"))) {
        printf("file open error.\n");
        return 1;
    }
    while(1) {
        printf("%08x ", addr);
        readnum = fread(buf, 1, 16, fp);
        /* パイナリデータの表示 */
        for(i = 0; i < readnum; i++) {
            if( i == 8)
                 printf("   ");
            printf("%02x ", buf[i]);
        }
        for(i =readnum; i < 16; i++) {
            if(i == 8)
                printf("   ");
            printf("   ");
        }
        printf("   ");
        for(i = 0; i < readnum; i++)
            printf("%c", (32 <= buf[i] && buf[i] <= 126) ? buf[i] : '.');
        printf("\n");
        addr += 16;
        if(feof(fp))
            break;
    }
    fclose(fp);
    return 0;
}

 

 

Calculator

電卓プログラムの考え方、書き方例

2つの数値の加減乗除をする電卓のようなプログラムを作ります。
簡単なようですが、多くの話題(データ型の選択、入力、判断や繰り返しなど)を含んでおり、学習に役立つ例題です。
今回は、プログラムをつくる流れも分かるように、どんなプログラムにするか検討するところから始めます。 それを処理手順に書き、C言語のコードに直します。

プログラムの機能を考える

まずは、どんなプログラムにするかを考え、前提とすることや制約についても検討します。

  1. 数や演算の指定はどうする?
    たとえば「2 + 5」のように、「数値1 演算記号 数値2」の順に入力すると計算結果が表示されるようにする。
    演算記号は、+, -, *, / のみとする。
  2. 繰り返して計算できるようにする
    繰り返しの終了は、指定が上記の書式でなかったときとしよう。
  3. 他に考えておくことは?
    (1) 浮動小数点数でも計算できるようにする
    (2) 加減乗除以外の演算記号が指定されたらどうする?
    (3) 割り算で、ゼロ割しようとしたときはどうする?
    (4) 電源ON の代わりに指定の書式を示すメッセージを表示しよう。電源OFF のメッセージも表示しよう
    (2) と (3) のケースは、エラー・メッセージを表示して、指定し直せるようにしよう。

処理手順として書く

上の検討結果を踏まえて、処理内容を具体的かつ端的に書きます。 判断や繰り返し、データの入出力も分かるようにします。
下記では、チャートを使わずに書きます。

電卓プログラムの処理手順 】
指定の書式を示すメッセージを表示する
<繰り返し>
|  「数値1 演算記号 数値2」の指定を受け取る –> 変数 a, op, b へ
|    指定の書式でなかったとき) 繰り返しを抜ける
|  演算記号 op に応じた計算をする –> ans へ結果を入れる
|    + のとき) a + b を求める
|    - のとき) a – b を求める
|    * のとき) a * b を求める
|    / のとき) b がゼロでないかチェックする
|          ゼロのとき) エラー・メッセージを表示して繰り返しの先頭に戻る
|          a / b を求める
|    それ以外) エラー・メッセージを表示して繰り返しの先頭に戻る
-  答え ans を表示する
電源OFF のメッセージを表示する

プログラムコードに直す

上記の手順を素直にC言語で書きます。

電卓プログラムの書き方例 】

#include <stdio.h>
main()
{
    double  a, b, ans;
    char    op;

    printf( "加減乗除(+,-,*,/)ができます。指定例:2+5、終了時はq\n" );
    while( 1 ) {
        printf( "ready : " );
        if( scanf( "%lf %c %lf", &a, &op, &b ) != 3 ) break;
        switch( op ) {
        case '+': ans = a + b; break;
        case '-': ans = a - b; break;
        case '*': ans = a * b; break;
        case '/': if( b == 0.0 ) {
                     printf( "Error!(ゼロでの割算はできません)\n" );
                     continue;
                  }
                  ans = a / b; break;
        default:  printf( "Error!(演算記号の指定が誤りです)\n" );
                  continue;
        }
        printf( "--> %g\n", ans );
    }
    printf( ".... Power OFF\n" );
}

 実行例

加減乗除(+,-,*,/)ができます。指定例:2+5、終了時はq
ready : 6.5 * 3
--> 19.5
ready : 7 % 4
Error!(演算記号の指定が誤りです)
ready : 123 / 2
--> 61.5
ready : 7 + 16
--> 23
ready : q
.... Power OFF

 

Reverse Polish Notation Calculator

逆ポーランド記法

逆ポーランド記法

逆ポーランド記法を使えば、式の計算をする(評価)には、先頭からひとつずつ順番に記号を読み込み、その記号が演算子以外であればスタックに値を積み、演算子であればスタックから値を取り出して演算し結果をスタックに積む、という簡単な操作の繰り返しだけでよい。そのため、プログラミング初心者の練習課題として、逆ポーランド記法の電卓を作ることがよく行われる。

逆ポーランド記法による計算の例

2+3を計算するとき,逆ポーランド記法では,次のように表す.数値や演算子(+, -, *, /)の間にはスペースを設ける.

2 3 +

これはいくつかのメモリー(記憶場所)が準備されているとき,

  • 2を1番目のメモリーに記憶
  • 3を2番目のメモリーに記憶
  • 1番目のメモリーの内容と2番目のメモリーの内容を加算
  • 加算結果を1番目のメモリーに記憶

という手順で計算することを表している.

特徴:

  1. 日本語の並びと同じ計算順序
  2. 逆ポーランドには括弧がない
  3. キータッチ数は最少

o0125007411785329651

【中置記法】
3 * ( 1 + 2 ) / ( ( 4 – 5 ) / ( 6 + 7 ) ) =  キータッチ数は22回

【逆ポーランド記法】
3 1 2 + * 4 5 – 6 7 + / /           キータッチ数は13回

問題

次のような機能を持つ電卓プログラムを実現せよ。

    1. 基本機能
      1. 演算子は+,-,*,/を許す。
      2. 被演算子にはdouble型を許す。
      3. 式の記法には逆ポーランド記法を用いよ。
      4. 演算子=により結果を表示する。
    2. 数学関数
      1. sin,cos,tan,exp,sqrなど

文字列解析、配列によるスタック実装など、総合的な演習問題としてよく使われます。K&Rにも同様の演習があります。

回答例

#define STACK_DEPTH 100
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<ctype.h>
#include<math.h>

double stack[STACK_DEPTH];
int sp=STACK_DEPTH;

double pop(void){ return (sp<STACK_DEPTH)?stack[sp++]:0.0; }
void push(double f){ if(sp>0)stack[--sp]=f; }
int getword(char* dst, const char* str, int limit) {
    int i, j, len = strlen(str);
    for(i=0; i<len && isspace(str[i]); i++);
    for(j=0; j<limit && (j+i)<len && !isspace(str[i+j]); j++) dst[j]=str[i+j];
    dst[j]='\0';
    return i+j;
}

int main(void)
{
    char line[100], tmp[100];
    int i, c;
    while(1){
        i = 0;
        fgets(line, 100, stdin);
        while((c=getword(tmp, &line[i], 100)) && strlen(tmp)){
            if(strcmp(tmp, "sin")==0) push(sin(pop())); else
            if(strcmp(tmp, "cos")==0) push(cos(pop())); else
            if(strcmp(tmp, "sqr")==0) push(pow(pop(),2)); else
            if(strcmp(tmp, "+")==0) push(pop()+pop()); else
            if(strcmp(tmp, "-")==0) push(pop()-pop()); else
            if(strcmp(tmp, "*")==0) push(pop()*pop()); else
            if(strcmp(tmp, "/")==0) push(pop()/pop()); else
            if(strcmp(tmp, "=")==0) printf("%f\n", pop()); else
            push(atof(tmp));
            i+=c;
        }
        if (line[0] == '\n') break;
    }
    return 0;
}

 

Nim game

Nim game

ニム (nim) は、2人で行うレクリエーション数学ゲームの1つである。ルーツは古代中国からあるとされ、16世紀初めの西欧で基本ルールが完成したが、名前については、一般的に1901年ハーバード大学のチャールズ・L.バウトン (Charles L. Bouton) によって名付けられたとされる。

ゲームルール:

一人1個か2個か3個か4個だけ取れて、交互にやっていって、
最後の1個の石を取った人が負けとなります。

#include <stdio.h>

int main(void)
{
  int i, stone, n=0, turn=0;
  char name[2][256];

  printf("Input players' names\n");

  printf("player 1:");
  gets(name[0]);
  printf("player 2:");
  gets(name[1]);

  printf("\nnumber of stones:");
  scanf("%d", &stone);

  while (stone > 0){
    printf("\nThere are %d stones.\n", stone);
    printf("%s's turn! Take some stones:", name[turn]);
    while (n < 1 || n > 4) scanf("%d", &n); //取る石の数を制限1..4
    stone -= n;
    turn = 1 - turn;
    n = 0;
  }

  printf("\nNow winner is %s!!\n", name[turn]);

  // getch();
  // return 0;
}

課題

勝者の名前、対戦記録をファイルに記録するように改造してください。記録ファイル名は、nim.scrとする。
例:
====================================
Input players’ names
player 1:tom
player 2:mary

Now winner is mary!!
====================================
nim.c、nim.scr と実行画面のコピーを提出してください。