Socket学习笔记之一

1.Socket Introduction

  socket,或称套接字,是一种interprocess communication channel,不仅可以在同一台机器的不同进程间通信,也可以通过网络实现不同主机之间的不同进程间通信,就是所谓的网络编程,也就是说socket其实就是一种IPC工具,只不过可以跨主机。

  既然socket可以满足不同的进程通信需求,那么编程的时候必定要指定一些规则,比如说是传送数据报文还是字节流、传送的时候数据丢失或乱序的处理等等,这就需要了解一下socket communication style,对于程序员来讲需要了解以下3个宏:

  SOCK_STREAM: 这个宏的意思就是规定字节流的方式通信,我们知道TCP就是一个实现了上层对传输层的字节流处理,而且通过TCP的流量控制和应答机制保证其通信的可靠性。

  SOCK_DGRAM: 这个宏的意思是以数据报文的方式通信,很容易让人想起来UDP这种非面向连接的网络通信协议,通信时只是通过IP packet传输,不保证通信的可靠性。

  SOCK_RAW: 这个宏表示要使用底层的网络通信,一般用不到。

 

2.Namespace and Name

  在socket编程中需要区分namespace和name,namespace直译为命名空间,但其实更合适的叫法是Protocol Family,表示一些通信协议的集合,因此它的实例名字一般是以PF开头,如PF_UNIX,PF_INET等等。创建一个socket的时候,需要先确定namespace再给它name,这里的name其实就是address,在本地进程间通信的时候就是指定一个文件路径,而网络通信的时候就是指定一个host IP address。

 

3.Data Structure

  在bind(),accept()等函数中可以看到这么一个数据结构struct sockaddr,这其实是一个generic data type:

1 struct sockaddr
23   short int sa_family;
4   char sa_name[14];
5 };

  实际上经常使用的数据类型是struct sockaddr_un,struct sockaddr_in,然后在使用bind等函数的时候将其强制类型转换成sockaddr类型。

#define UNIX_PATH_MAX    108

struct sockaddr_un {
    sa_family_t sun_family;    /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX];    /* pathname */
};

struct sockaddr_in {
  sa_family_t        sin_family;    /* Address family        */
  __be16        sin_port;    /* Port number            */
  struct in_addr    sin_addr;    /* Internet address        */

struct in_addr {
    __be32    s_addr;
};

  

4. Examples

4.1 同一主机不同进程间的通信

"server" code:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <sys/types.h>
  4 #include <sys/socket.h>
  5 #include <sys/un.h>
  6 #include <string.h>
  7 
  8 #define handle_error(msg)   \
  9         do{                 \
 10             perror(msg);    \
 11             exit(1);        \
 12         }while(0)
 13 #define SOCK_FILE_PATH  "./sockfile"
 14 
 15 int main( int argc, char *argv[] )
 16 {
 17     int ser_fd, cli_fd;
 18     struct sockaddr_un ser_addr, cli_addr;
 19     socklen_t cli_addr_len;
 20 
 21     /*--Create a socket.--*/
 22     ser_fd = socket( AF_UNIX, SOCK_STREAM, 0 );
 23     if( ser_fd < 0 )
 24     {
 25         handle_error( "Error socket" );
 26     }
 27     /*--Bind.--*/
 28     ser_addr.sun_family = AF_UNIX;
 29     strcpy( ser_addr.sun_path, SOCK_FILE_PATH );
 30     if( bind( ser_fd, ( struct sockaddr * )&ser_addr, sizeof( struct sockaddr_un ) )  == -1 )
 31     {
 32         handle_error( "Error bind" );
 33     }
 34     /*--Listen.--*/
 35     if( listen( ser_fd, 20 ) == -1 )
 36     {
 37         handle_error( "Error listen" );
 38     }
 39     puts( "Server listening...\n" );
 40     /*--Accept.--*/
 41     cli_fd =  accept( ser_fd, ( struct sockaddr * )&cli_addr, &cli_addr_len );
 42     if( cli_fd < 0 )
 43     {
 44         handle_error( "Error accept" );
 45     }
 46     puts( "Accept one request.\n" );
 47     /*--Read and write socket file.--*/
 48     write( cli_fd, "Hello client!\n", sizeof( "Hello client!\n" ) );
 49     puts( "Write done.\n" );
 50 
 51     close( cli_fd );
 52     puts( "Close the connection.\n" );
 53 
 54     /*--Close client socket file descriptor.--*/
 55     return 0;
 56 }

"client" code:

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/socket.h>
  4 #include <sys/un.h>
  5 

  7 #define READ_BUFF_LENGTH    100
  8 int main( int argc, char *argv[] )
  9 {
 10     int cli_fd;
 11     struct sockaddr_un ser_addr;
 12     char readbuf[READ_BUFF_LENGTH];
 13     /*--Create a socket file.--*/
 14     ser_addr.sun_family = AF_UNIX;
 15     strcpy( ser_addr.sun_path, SOCK_FILE_PATH );

 18     cli_fd = socket( AF_UNIX, SOCK_STREAM, 0 );

 25     /*--Try to connect to the server.--*/
 26     if( connect( cli_fd,  ( struct sockaddr * )&ser_addr, sizeof( struct sockaddr ) ) == -1 )
 27     {
 28         perror( "Error connect" );
 29         return -1;
 30     }
 31     /*--Read data from socket file.--*/
 32     puts( "Connection success.\n" );
 33     read( cli_fd, readbuf, READ_BUFF_LENGTH );
 34     printf( "%s", readbuf );
 35 
 36     return 0;
}

  在两个不同的终端分别运行两个程序,首先运行服务器程序,然后运行客户端程序,可以看到客户端界面读到了来自服务器的数据。另外,这里的读写依旧使用的是read、write函数,进一步证明了GNU/Linux中“一切皆文件”。

4.2 Socket Internet Communication

server code:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <sys/types.h>
  4 #include <sys/socket.h>
  5 #include <string.h>
  6 #include <arpa/inet.h>
  7 #include <netinet/in.h>
  8 
  9 #define handle_error(msg)   \
 10         do{                 \
 11             perror(msg);    \
 12             exit(1);        \
 13         }while(0)
 14 
 15 int main( int argc, char *argv[] )
 16 {
 17     int ser_fd, cli_fd;
 18     struct sockaddr_in ser_addr, cli_addr;
 19     socklen_t cli_addr_len;
 20 
 21     /*--Create a socket.--*/
 22     ser_fd = socket( AF_INET, SOCK_STREAM, 0 );
 23     if( ser_fd < 0 )
 24     {
 25         handle_error( "Error socket" );
 26     }
 27     /*--Bind.--*/
 28     ser_addr.sin_family = AF_INET;
 29     ser_addr.sin_port = htons( 8080 );
 30     ser_addr.sin_addr.s_addr = INADDR_ANY;
 31     if( bind( ser_fd, ( struct sockaddr * )&ser_addr, sizeof( struct sockaddr_in ) )  == -1 )
 32     {
 33         handle_error( "Error bind" );
 34     }   
 35     /*--Listen.--*/
 36     if( listen( ser_fd, 20 ) == -1 )
 37     {
 38         handle_error( "Error listen" );
 39     }
 40     puts( "Server listening...\n" );
 41     /*--Accept.--*/
 42     cli_fd =  accept( ser_fd, ( struct sockaddr * )&cli_addr, &cli_addr_len );
 43     if( cli_fd < 0 )
 44     {
 45         handle_error( "Error accept" );
 46     }
 47     puts( "Accept one request.\n" );
 48     /*--Read and write socket file.--*/
 49     write( cli_fd, "Hello client!\n", sizeof( "Hello client!\n" ) );
 50     puts( "Write done.\n" );
 51 
 52     close( cli_fd );
 53     puts( "Close the connection.\n" );
 54 
 55     /*--Close client socket file descriptor.--*/
 56     return 0;
 57 }

client code:

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/socket.h>
  4 #include <netinet/in.h>
  5 
  6 #define READ_BUFF_LENGTH    100
  7 int main( int argc, char *argv[] )
  8 {
  9     int cli_fd;
 10     struct sockaddr_in ser_addr;
 11     char readbuf[READ_BUFF_LENGTH];
 12     /*--Create a socket file.--*/
 13     ser_addr.sin_family = AF_INET;
 14     ser_addr.sin_port = htons( 8080 );
 15     ser_addr.sin_addr.s_addr = inet_addr( "127.0.0.1" );
 16 
 17     cli_fd = socket( AF_INET, SOCK_STREAM, 0 );
 18     /*--Try to connect to the server.--*/
 19     if( connect( cli_fd,  ( struct sockaddr * )&ser_addr, sizeof( struct sockaddr_in ) ) == -1 )
 20     {
 21         perror( "Error connect" );
 22         return -1;
 23     }
 24     /*--Read data from socket file.--*/
 25     puts( "Connection success.\n" );
 26     read( cli_fd, readbuf, READ_BUFF_LENGTH );
 27     printf( "%s", readbuf );
 28 
 29     return 0;
 30 }

  写这个小程序时开始有个错误:connection refused,后来发现是server的sockaddr_in结构体的成员赋值问题,只要ser_addr.sin_addr.s_addr = INADDR_ANY即可,无需调用inet系列的函数进行转换。另外,搜索相关的错误信息时发现有人因为没有调整server和client的端口号字节序而报错。

  上面的程序通过使用file IO实现了网络通信,但是这样写并不规范,可移植性也不好,在TCP中使用send函数取代write函数,recv函数取代read函数,在UDP中则使用其他的API,如sendto、readfrom等。另外,以上的服务器程序在与客户端进行一次通信之后即关闭了,而一个正常的服务器是时刻准备着,所以可以用无限循环来实现。改进的服务器端代码如下:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <sys/types.h>
  4 #include <sys/socket.h>
  5 #include <string.h>
  6 #include <arpa/inet.h>
  7 #include <netinet/in.h>
  8 
  9 #define handle_error(msg)   \
 10         do{                 \
 11             perror(msg);    \
 12             exit(1);        \
 13         }while(0)
 14 
 15 int main( int argc, char *argv[] )
 16 {
 17     int ser_fd, cli_fd, connect_cnt = 0;
 18     struct sockaddr_in ser_addr, cli_addr;
 19     socklen_t cli_addr_len;
 20 
 21     /*--Create a socket.--*/
 22     ser_fd = socket( AF_INET, SOCK_STREAM, 0 );
 23     if( ser_fd < 0 )
 24     {
 25         handle_error( "Error socket" );
 26     }
 27     /*--Bind.--*/
 28     ser_addr.sin_family = AF_INET;
 29     ser_addr.sin_port = htons( 8080 );
 30     ser_addr.sin_addr.s_addr = INADDR_ANY;
 31     if( bind( ser_fd, ( struct sockaddr * )&ser_addr, sizeof( struct sockaddr_in ) )  == -1 )
 32     {
 33         handle_error( "Error bind" );
 34     }
 35     /*--Listen.--*/
 36     if( listen( ser_fd, 20 ) == -1 )
 37     {
 38         handle_error( "Error listen" );
 39     }
 40     puts( "Server listening...\n" );
 41     /*--Accept.--*/
 42     while( 1 )
 43     {
 44         cli_fd =  accept( ser_fd, ( struct sockaddr * )&cli_addr, &cli_addr_len );
 45         if( cli_fd < 0 )
 46         {
 47             handle_error( "Error accept" );
 48         }
 49         puts( "Accept one request.\n" );
 50         connect_cnt++;
 51         /*--Read and write socket file.--*/
 52         send( cli_fd, "Hello client!\n", sizeof( "Hello client!\n" ), 0 );
 53         printf( "%dth connection.\n", connect_cnt );
 54         close( cli_fd );
 55         printf( "Close the connection.\n" );
 56     }
 57     /*--Close client socket file descriptor.--*/
 58     return 0;
 59 }

  其实一个服务器进程完全可以写成一个daemon task的。

 

4.3 实现一个基于socket的简易http server

  实现的代码和上面并没有太大差别,不过需要本地提供html文件,我不会写网页编程,就直接通过firefox浏览器保存网页代码的方式得到了--。--,服务端只需要将本地的html文件内容发给客户端即可,由客户端完成其解释过程,因此客户端就不需要自己实现了,直接用浏览器即可。下面是服务端代码:

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <fcntl.h>
  4 #include <sys/stat.h>
  5 #include <sys/types.h>
  6 #include <sys/socket.h>
  7 #include <arpa/inet.h>
  8 
  9 #define WRITE_BUF_LEN   ( 64 * ( 2 << 10 ) )
 10 #define INDEX_PATH      "index.html"
 11 #define handle_error(msg)   \
 12         do                  \
 13         {                   \
 14             perror(msg);    \
 15             return 1;       \
 16         }while(0)
 17 
 18 int main( int argc, char *argv[] )
 19 {
 20     char write_buff[WRITE_BUF_LEN];
 21     int server_fd, client_fd;
 22     int index_fd;
 23     struct sockaddr_in server_addr, client_addr;
 24     socklen_t addr_len = sizeof( struct sockaddr );
 25 
 26     server_addr.sin_family = AF_INET;
 27     server_addr.sin_port = htons( 80 );
 28     server_addr.sin_addr.s_addr = INADDR_ANY;
 29 
 30     /*--Create a socket.--*/
 31     server_fd = socket( AF_INET, SOCK_STREAM, 0 );
 32     if( server_fd < 0 )
 33     {   
 34         handle_error( "socket error" );
 35     }
 36     /*--Bind an address to the newly-created socket.--*/
 37     if( bind( server_fd, ( struct sockaddr * )&server_addr, addr_len ) == -1 )
 38     {
 39         handle_error( "bind error" );
 40     }
 41     /*--Server listen.--*/
 42     if( listen( server_fd, 20 ) == -1 )
 43     {
 44         handle_error( "listen error" );
 45     }
 46     printf( "Http server listening...\n" );
 47 
 48     while( 1 )
 49     {
 50     /*--Accept an client request.--*/
 51         client_fd = accept( server_fd, ( struct sockaddr * )&client_addr, &addr_len );
 52         if( client_fd < 0 )
 53         {   
 54             handle_error( "accept error" );
 55         }
 56     /*--Open the local html flie.--*/
 57         index_fd = open( INDEX_PATH, O_RDWR, 0600 );
 58         if( index_fd < 0 )
 59         {   
 60             handle_error( "open error" );
 61         }
 62     /*--Read index.html, and send the content to the client.--*/
 63         if( read( index_fd, write_buff, WRITE_BUF_LEN ) == -1 )
 64         {   
 65             handle_error( "read error" );
 66         }
 67         if( send( client_fd, write_buff, WRITE_BUF_LEN , 0 ) == -1 )
 68         {
 69             handle_error( "send error" );
 70         }
 71     /*--Close the connection.--*/
 72         close( client_fd );
 73     }
74 close( server_fd );
75 }

  这个最最简单的http服务器只能提供很简单的服务,没有使用多线程,也不能加载图片等基本的多媒体信息,有时间我会再研究一下tinyhttp的源码。

5. Summary

  socket不仅可以用来网络编程,也可以用作普通的IPC元件。这次的学习内容多半是网络编程,实现了不同主机之间的TCP通信,最后还实现了一个基于socket的http服务器,虽然功能极其简单,但是也初步体会了http的基本原理,使我更加熟悉了GNU/Linux应用编程。

 

posted @ 2017-08-19 19:47  Moosee  阅读(159)  评论(0)    收藏  举报