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 2{ 3 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应用编程。

浙公网安备 33010602011771号