一.套接字编程
API函数介绍
SOCKET accept( SOCKET s , struct sockaddr_in FAR * addr ,int Far *addlen ) ;
函数说明:当没有连接请求时,对于阻塞式套接字,如果程序调用了accept函数,那么线程将进入等待状态,知道有一个连接请求到达为止,accept在接收到连接请求时,会为这个连接建立起一个新的套接字,该套接字负责和客户单进行通信,常被称为“会话套接字。此前调用的socket函数返回的套接字负责监听和接收连接请求,因此被称为“监听套接字”。
参数说明:参数s是此前用于监听和接收连接的“监听套接字”。
参数addr和参数addlen是用于返回客户机的信息,如果服务机对此不关心,那 么可以都设置为NULL。
int connect( SOCKET s , struct sockaddr_in FAR * name , int namelen ) ;
参数说明:参数s是调用socket函数返回的套接字。
参数name 是服务机的地址,namelen属于服务机地址类型的大小。
int send( SCOKET s , const char FAR *buf ,int len , int flags ) ;
int recv( SOCKET s , char FAR * buf , int len , int flags ) ;
参数说明:参数s在服务器端指调用accept函数接收客户端的连接请求后返回的会话套接字,而客户端只有一个会话套接字(因为客户端不用监听连接请求,因此没有监听套接字)
参数buf是指要发送和接收数据的缓冲区。
参数len为buf的大小。
如果函数调用成功,会返回实际发送或接收的字节数。
如果函数调用失败,那么将返回错误SOCKET_ERROR。
二.利用socket实现代理服务器的基本框架
代理服务器是基于socket套接字编程实现的,现在可以考虑实现三个端,一个是client , 一个是server,最后一个是proxy server。
其中proxy server就是我们要实现的代理服务器,在这一步中实现的主要功能是并没有实现能够解析HTTP请求的机制(这个功能实际上是比较简单的),但是大体框架是利用多线程编程接收客户端的请求,发送给服务器,接收服务器的响应,发送给客户端。
三个基本程序都是基于多线程的套接字编程。下一步将加入HTTP协议的内容,那么就可以实现HTTP代理的作用了。
主要的代码需呀说明一下:
proxyThread是一个线程函数,是这个代理服务器的主体框架和核心代码。
// 代理服务器线程,处理与客户端和目标服务器的连接
DWORD WINAPI proxyThread( LPVOID lp )
{
ThreadPara para = *(ThreadPara*)lp ;
SOCKET clientsocket = para.clientsocket ;
SOCKET serversocket = para.serversocket ;
SOCKET proxysocket ; // 创建一个监听套接字,用户接受目标服务器的响应,处理与目标服务器的通信
//定义接收来至客户端的数据
char client_buffer[4096] ;
int len = recv( clientsocket , client_buffer , MAXBYTE , NULL ) ;
if( len < 0 )
{
printf("接收客户端数据失败!\n") ;
return 0 ;
}
client_buffer[len] = '\0' ;
//输出客户端发来的数据
printf("client addr :%d , port = %d\n" , para.client_addr.sin_addr.S_un.S_addr , para.client_addr.sin_port ) ;
printf("from client data :\n%s\n" , client_buffer ) ;
// 填充目标服务器的地址
sockaddr_in server_addr ;
server_addr.sin_family = AF_INET ;
server_addr.sin_addr.S_un.S_addr = inet_addr( "127.0.0.1" ) ;
server_addr.sin_port = htons( 1234 ) ;
// 连接目标服务器
// 为了与目标服务器建立连接,必须再建立一个与套接字serversocket不同的套接字来实现
proxysocket = socket( AF_INET , SOCK_STREAM , 0 ) ;
connect( proxysocket , (SOCKADDR*)&server_addr , sizeof( SOCKADDR ) ) ;
// 把接收来至客户端的数据发给目标服务器
send( proxysocket , client_buffer , sizeof( client_buffer ) , NULL ) ;
// 接收来至目标服务器的响应
char server_buffer[4096] ;
int len2 = recv( proxysocket , server_buffer , MAXBYTE , NULL ) ;
if( len2 < 0 )
{
printf("接收服务器端的数据失败!\n") ;
return 0 ;
}
server_buffer[len] = '\0' ;
// 输出目标服务器端的响应
printf("server address:%d , port = %d\n" , server_addr.sin_addr.S_un.S_addr , server_addr.sin_port ) ;
printf("from server data : \n%s\n" , server_buffer ) ;
// 将接收来至服务器端的数据发给相应的客户端
send( clientsocket , server_buffer , sizeof( server_buffer ) , NULL ) ;
// 关闭套接字
closesocket( clientsocket ) ;
closesocket( serversocket ) ;
closesocket( proxysocket ) ;
return 0 ;
}
为了方便编程,设计了多线程参数类型:
// 定义线程参数的类型
typedef struct ThreadPara
{
SOCKET serversocket ; // 本地监听套接字,用于处理与客户端的连接
SOCKET clientsocket ; // accept函数返回的套接字,用于处理与客户端的连接
sockaddr_in client_addr ; // 客户端地址
} ThreadPara ;
主函数部分:
因为代理服务器的作用是帮助客户端去访问目标服务器,那么在主函数中代码的作用就是在一个循环中不断等待客户端的连接,然后对每一个连接都创建一个线程去处理它。
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA ws ;
WSAStartup( MAKEWORD( 2 , 2 ) , &ws ) ;
SOCKET serversocket , clientsocket ;
serversocket = socket( AF_INET , SOCK_STREAM , 0 ) ;
if( INVALID_SOCKET == serversocket )
{
printf(" Create serversocket error !\n") ;
return 0 ;
}
// 填充本地地址并绑定
sockaddr_in local_addr ;
local_addr.sin_family = AF_INET ;
local_addr.sin_addr.S_un.S_addr = inet_addr( "127.0.0.1") ;
local_addr.sin_port = htons( 5678 ) ;
bind( serversocket , (SOCKADDR*)&local_addr , sizeof( SOCKADDR ) ) ;
//监听
listen( serversocket , 10 ) ;
// 接收客户端的请求并建立连接
sockaddr_in client_addr ;
int size = sizeof( SOCKADDR ) ;
while( true )
{
clientsocket = accept( serversocket , (SOCKADDR*)&client_addr , &size ) ;
if( INVALID_SOCKET == clientsocket )
{
printf("clientsocket error!\n") ;
return 0 ;
}
// 创建客户端线程处理与客户端的连接
ThreadPara clientPara ;
clientPara.clientsocket = clientsocket ;
clientPara.client_addr = client_addr ;
// 创建处理线程
HANDLE hClient = CreateThread( NULL , 0 , proxyThread , &clientPara , 0 , NULL ) ;
WaitForSingleObject( hClient , INFINITE ) ; //等待所有线程结束
}
WSACleanup() ;
return 0;
}
三.HTTP代理服务器的实现
为了说明实际中代理服务器是如何工作的,现在来了解一下,浏览器访问目标服务器的整个过程。
在不使用代理服务器的情况下,只需要在浏览器的搜索框中输入响应的URL,那么就可以访问到自己想要得到内容。在这个过程中,首先本地浏览器会解析URL,然后构造HTTP请求数据包,然后发送给目标服务器。
但是在使用代理服务器的情况下,本地浏览器会把构造好的HTTP协议数据包发给代理服务器,代理服务器会解析这个HTTP流量包,然后把这个包发给目标服务器。
根据这个过程,那么就可以确定代理服务器的具体功能了。
代理服务器可以有各种各样的功能,我们这里只关心并只会实现这个功能,如何解析HTTP请求包。
演示:
为了方便演示,直接在客户端的程序中给出请求包的内容:
char buffer[] = { "Get / HTTP/1.1\nhost:127.0.0.1:1234\naccept:\n" } ;
这个是客户端发给服务器端的请求
这个是HTTP代理服务器记录的中间请求
这个是服务器发给客户端的响应
程序说明:
在代理服务器中主要是对客户单请求的解析,然后得到目标服务器的IP地址和端口号,将请求转发给目标服务器。
为了方便编程,设计了如下数据结构:
// 定义解析出来的目标主机的地址
typedef struct ServerInfo
{
char IP[20] ; // 目标主机的IP地址
int port ; // 目标主机的端口号
} ServerInfo ;
解析请求的代码如下,都是字符串的操作,在C语言编程中,字符串的操作是很重要的,要熟练掌握。
bool MyHttpParaser( char * request , ServerInfo & serverInfo )
{
char requestLine[50] ;
char * pos = strstr( request , "host:" ) ;
pos = pos + 5 ;
int i = 0 ;
while( *pos != '\n' )
{
requestLine[i] = *pos ;
pos = pos + 1 ;
i++ ;
}
requestLine[i] = '\0' ;
pos = strstr( requestLine , ":" ) ;
*pos = '\0' ;
pos = pos + 1 ;
char port[20] ;
strcpy( port , pos ) ;
strcpy( serverInfo.IP , requestLine ) ;
serverInfo.port = atoi( port ) ; // 将端口号转换为int型
printf("serverInfo.IP = %s , serverInfo.Port = %d\n" , serverInfo.IP , serverInfo.port ) ;
return true ;
}
在服务器端那么就是对客户端的请求作出解析后给出适当地响应。
这个在我的一篇博文利用socket自己实现基于HTTP协议的Web服务器中已经给出了代码和解释。
——————————————————————————————————————————————————————————————————————————————
那么现在,HTTP代理服务器已经解析完毕,现在主要就是把它与浏览器来一起进行设置了,这个将在后续的文章中给出。