网络编程(一)
近来在做P2P下载工具,复习一下网络编程。
通信网络类型
所有的通信网络就其实现技术可以分成两种:线路交换和包交换。
其中线路交换就是传输时发送端和接收端之间建立一个特定的线路连接,数据在此线路上进行传输,比如电话。
包交换就是发送端将数据分割为块,处理后形成一个包Packet,将包在网络中单独传输,包中除了传输的数据以外,还有发送端地址,接收端地址,以及端口,文件大小等类似的必要信息。典型的就是计算机网络。
7层网络协议
基于ISO(国际标准化组织)提出的OSI标准,也就是七层通信协议标准,我们将网络协议分为7层:
- 应用层Application:最接近用户的层,一般是构架在通信协议以上的网络应用软件,直接与用户交互。
- 表示层Presentation:作用是将不同计算机的编码方式统一起来,并且对数据进行一些压缩,加密等工作。
- 会话层Session:控制数据传输时的数据交换,包括传输方向,中断处理等。
- 传输层Transport:控制数据流保证数据正确传输,屏蔽硬件变动和差异。
- 网络层NetWork:在发送器和接收端创建一个虚拟路径,实现数据包的路由。
- 数据链路层DataLink:对底层数据进行封包,将上层数据进行分割为桢。
- 物理层Physical:控制数据在物质媒介上的传输。
TCP/IP
而我们常听到的TCP/IP协议是开发时的一系列协议,作为一中网络标准控制我们的开发,此协议主要分为四层:
- 应用层:相当于OSI模型的应用层+表示层+会话层。
- 传输层:相当于OSI模型中的传输层。
- 网络层:相当于OSI模型中的网络层。
- 网络访问层:相当于OSI中的数据链路层+物理层。
但是值得我们记住的,所谓的TCP/IP,通常并不是仅仅包括TCP和IP这两个协议,通常还包括其他协议,包括FTP,SMTP,HTTP,TFTP,UDP,TCMP,ARP,RARP,TELENT等协议,这些一起被称为协议簇。
UDP VS TCP
因为不是职业做网络协议方面的,所以这里不详细解释其他协议的含义,我们重点说下TCP协议和UDP协议的区别。
TCP协议安全性高,它要求两台计算机中的信息字节流无差错的进行交互,所以两者间必须建立有效的连接。
UDP协议安全性低,它是无连接的协议,不需要与对方建立连接,仅仅是将数据包发送过去,对方是否会遗漏数据,它不关心,不进行数据完整性安全性检查。然而在传输少量数据时,UDP速度比较快。
Socket套接字
我们首先可以理解,网络间的通信,在简化之后,不过是网络间两两端点之间的通信,而Socket就是通信端点的一种抽象,提供了一种发送和接收数据的机制。
Socket创建后,可以通过网络与其他的Scoket创建应用程序通信,就类似于一个下水道的两个端口,只要这个端口找到了另外一个端口,在他们之间就可以铺设一个信息管道,进行信息传递。
而管道口是既可以出水也可以入水的,Scoket做为网络上的I/O基础,可以进行信息的输入输出工作。但若我们需要与另外一个计算机进行连接,我们需要知道它的IP和Port端口号,而这个信息就存储在Socket中,我们可以理解为管道口那里也挂着个门牌号一样。
在这里我们就可以解释上面UDP/TCP的区别所在了。我们是使用Socket来进行网络间通信。
而Scoket可分为两类,Socket_Stream流Socket和Socket_Dgram数据报Socket,而这两种Socket的区别在于,流Scoket是双向的,有序的,无重复的,无边界记录的数据流。而数据报Socket不保证可靠,不保证有序,不保证重复性,它建立的就是UDP之类的不可靠通信。
C/S体系与P2P体系
Client/Server客户端/服务器模式是很早就为网络接受的一种模型,他的实质就是服务器上保存资源,用户客户端与其通信,Server端进行数据包发送,Client进行接收。
网络游戏中则是玩家用户先对服务器进行数据传输,反馈玩家当前按键消息,服务器根据消息进行判断,数据库运算反馈信息,用户计算机接收信息,并进行相应的图形显示。例如:玩家按下’A’键,对应一个动作信息–>服务器判断,该动作信息能够触发什么事件,例如攻击了,怪物少了108的HP,玩家消耗12点魔法力,再将这些处理后的信息发送给玩家–>玩家计算机收到信息,进行显示,怪物血槽减少,玩家蓝色魔法槽减少等。
这些都是在两台计算机之下进行的交互通信,而且各计算机负责的功能不同,身份不同。 而P2P(Peer-to-Peer)原意就是平等to平等,也就是说,互联的两台计算机,是完全平等的,进行发送的一方也进行接收,这样就大大加强了用户之间的资源流通,同时也减少了单一服务器的压力,ServerLass。
现在我要做的就是将游戏的客户端下载,更新等功能实现P2P方式下载,不再是单纯的FTP下载,减少服务器压力。
C/S通信编码
一:Client端设计思路
创建Socket->连接服务器->发送接收数据->关闭Scoket。
二:Server端设计思路
创建Socket->绑定Socket->监听Socket->建立连接->接收发送数据->关闭Socket
WinSocket编程入门API
WinSocket规范实际上是Microsoft和一些公司共同制定的一套WINDOWS下的编程接口。注意,他不是TCP/IP那样的协议,仅是一套编程接口。
我们在使用WinSock编程时,需要包含WinSock API的头文件winsock.h,现在是winsock2.h,并连接库文件ws2_32.lib。
按照上面的设计思路,我来复习下相关API。
客户端编程
- 首先,调用WSAStartup()函数来和Winsock的动态连接库建立关系,然后调用socket来建立一个TCP或UDP Socket,使用bind()来手动指定IP地址和port端口号。(也可不指定,使用系统分配,之后可以使用getsockname()获取该值)
- 之后发送连接请求,调用connect()函数,此过程应该是循环的,直到成功,或者连接次数超时失败。
- 进行数据传输,因为暂不考虑使用UDP Socket,所以仅谈TCP Socket。发送函数send(),接收函数recv();
- 结束Socket连接。closesocket()函数,之后断开释放与Winsock库关系WSACleanup();
服务器端编程
- 依旧是初始化WinSocket DLL,WSAStartup();成功后我们可以使用该DLL中的API。
- 创建Socket,Socket();
- 绑定端口,对这个监听的Socket指定端口,这样的话,当客户端再次连接时,就可以知道连接哪个地址和端口。bind();
- 设置监听,listen();(注意,监听数量最大为5),这个应该是持续的,直到接收到Client的Connect请求。
- 接受连接:当监听到Client的请求之后对其IParam进行分析,若允许连接,则accept(),该函数会生成一个新的Socket与客户端的Socket进行连接,而原本的Socket将继续监听等待下一个连接请求。
- 关闭连接。当发现连接不合法不准备接受连接,或者服务器关闭整修时,则可closesocket()关闭监听socket,此函数无论客户端还是服务器端调用,都会中断连接。最后依旧调用WSACleanup()来通知WinStack来释放socket资源。
下面用个简单的代码进行说明。
/*========================
*客户端WinSocketAPI使用Demo
*Edit by 自由骑士笃志
*Mail:
*=======================*/
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
// 宏设置端口号
#define PORT_NUM 9000
void main( int argc,char **argv )
{
WSADATA wsaData; // 结构体,保存WinSocket库信息
SOCKET Socket; // Socket对象
SOCKADDR_IN serverAddr; // 结构体,保存Socket信息
int nPort = PORT_NUM; // 端口号
int nFileLength; // 文件长度
if ( argc <= 1 )
{
printf( "USAGE:filename<Server IP address>.\n" );
return;
}
/*
* 函数名称:WSAStartup()
*
* 函数作用:连接Winsock库
*
* 函数说明:
* WSAStartup该函数参数意义为:第一个参数是Socket版本号,
* 现在使用的是2.2版,假如想使用2.1版,则MAKEWORD( 2, 1 ),
* 第二个参数是out的,它是记录版本信息的一个指针,之后我们
* 会用到,该函数会将该版本号的Socket捆绑到程序中,若成功,
* 则返回0.
*/
if( ( WSAStartup( MAKEWORD( 2,2 ), &wsaData ) ) != 0 )
{
printf( "连接Winsock库失败 \n" );
return;
}
// 创建Socket并记录对象
/*
* 函数名称:socket()
*
* 函数作用:创建Socket并记录对象
*
* 函数说明:
* socket()用来生成一个新的Socket,其参数一为PF_INET或AF_INET,
* 只有这两个可选,PF代表协议族,AF代表地址族,实际意义一样,无需多理.
* 参数二为Socket类型,之前我们说过Socket分为流Scoket(SOCK_STREAM)
* 以及数据报Scoket(SOCK_DGRAM),第三个参数是通讯协议,若不指定的话
* 一般就设置为0,该函数若调用成功,则返回Socket对象,若失败则返回
* INVALID_SOCKET,失败时可使用WSAGetLastError()获取失败的原因.
*/
if( ( Socket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ) ) == INVALID_SOCKET )
{
printf( "创建套接字失败 %d\n",
WSAGetLastError() );
WSACleanup();
return;
}
/* 注:SOCKADDR_IN结构还有个参数是sin_zero,我这里没有设置
* 一般来说我们可以用bzero()或memset()函数将其置为零
* (它用来将sockaddr_in结构填充到与struct sockaddr同样的长度)不理解也可
*/
serverAddr.sin_family = AF_INET; // 地址族
serverAddr.sin_port = htons( nPort ); // 端口号
serverAddr.sin_addr.s_addr = inet_addr( argv[1] ); // IP地址
/* 注:这里的htons是字节转换,分离出中间的to之后,可按照下表分析
* h表示"host" ,n表示"network",s 表示"short",l表示 "long"。
*/
printf( "正在连接服务器,请稍候 %s : %d...\n",
inet_ntoa( serverAddr.sin_addr ), htons( serverAddr.sin_port ) );
/*
* 函数名称:connect
*
* 函数作用:对另一Socket提出连接申请
*
* 函数说明:
* connect()该函数第一个参数是本机socket信息,第二个参数是想要连接的
* 对方地址,第三个参数是对方地址的信息长度,该函数成功则返回0
* 失败则返回SOCKET_ERROR
*/
if( connect( Socket,( SOCKADDR* )&serverAddr, sizeof( serverAddr )) == SOCKET_ERROR )
{
printf("连接服务器失败 %s \n",WSAGetLastError());
closesocket( Socket );
WSACleanup();
return;
}
printf( "连接成功,更新数据中,请稍候...\n" );
/*
* 函数名称:socket()
*
* 函数作用:向另一Socket发送数据
*
* 函数说明:
* send()函数用于TCP Scoket数据发送,其中第一个参数是本机Socket对象(识别码)
* 第二个参数是即将发送的信息缓冲区的指针,第三个是即将发送的信息缓冲区大小
* 我们可以使用setsockopt/getsockopt来设置和获取缓冲区信息
* 第四个参数是函数调用类型,一般默认为0则可.此函数成功则返回输出信息文件的大小
* 这里值得注意的,这个大小未必和信息缓冲区大小一致,也未必和你准备输出的文件
* 大小一致,所以我们需要使用这个长度和准备发出的文件大小比对,以确保没有数据丢失
* ,此函数失败则返回SOCKET_ERROR
*/
if( ( nFileLength = send( Socket, "Hi,from a client.", 17, 0 ) ) == SOCKET_ERROR )
{
printf ( "发送数据失败 %s \n",WSAGetLastError() );
closesocket( Socket );
WSACleanup();
return;
}
/*
* 函数名称:closesocket()
*
* 函数作用:结束Socket连接,释放Socket对象
*
* 函数说明:结束Socket连接,参数是本机Socket对象(识别码),
* 该函数重载的另外一个声明是无参的
*/
printf("数据发送成功! %d bytes.\n",nFileLength );
closesocket( Socket );
/*
* 函数名称:WSACleanup()
*
* 函数作用:终止对WinSocketDLL的调用
*
* 函数说明:若在多线程中调用该函数,它将会终止WinSocket在所有线程中的使用
*/
WSACleanup();
}