网络编程
网络程序的实现可以有多种方式,Windows Socket就是其中一种比较简单的实现方法.Socket是连接应用程序与网络驱动程序的桥梁,Socket在应用程序中创建.通过绑定操作与驱动程序建立关系。此后应用程序送给Socket的数据,由Socket交给驱动程序向网络上发送出去.计算机从网络上收到与该Socket绑定的IP地址和端口号相关的数据后,由驱动程序交给Socket,应用程序便可从该Socket中提取接收到的数据。网络应用程序就是这样通过Socket进行数据的发送与接收的。
两台主机要进行通信,要遵循约定的规则,我们把这种规则称之为协议。为了标识在计算机上运行的每一个网络通信程序,为他们分别分配一个端口号。在发送数据时,除了指定接收数据的主机IP地址外,还要指定相应的端口号。这样,在指定IP地址的计算机上,将会由在指定端口号上等待数据的网络应用程序接收数据。
传输层:传输控制协议(TCP),用户数据报协议(UDP)
TCP:面向连接的可靠的传输协议。利用TCP协议进行通信时,首先要通过三步握手,以建立通信双方的连接。一旦连接建立好,就可以进行通信了。TCP提供了数据确认和数据重传的机制,保证了发送的数据一定能到达通信的对方。
UDP:是无连接的、不可靠的传输协议。采用UDP进行通信时,不需要建立连接,可以直接向一个IP地址发送数据,但是对方能否收到,就不敢保证了。我们知道在网络上传输的是电信号,既然是电信号,在传输过程中就会有衰减,因此数据有可能在网络上就消失了,也有可能我们所指定的IP地址还没有分配,或者该IP地址所对应的主机还没有运行,这些情况都有可能导致发送的数据接收不到。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区).应用程序通过系统调用与端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出。端口用一个整数型标识符来表示,即端口号。端口号跟协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立。端口使用一个16位的数字来表示,它的范围是0-65535,1024以下的端口号保留给预定义的服务。例如,http使用80端口。
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存)。基于Intel的CPU,即我们常用的PC机采用的是低位先存。为保证数据的正确性,在网络协议中需要指定网络字节顺序,TCP/IP协议使用16位整数和32位整数的高位先存格式。由于不同的计算机存放数据字节的顺序不同,这样发送方发送数据后,即使接收方接收到该数据,也有可能无法查看所接收到的数据。所以在网络中不同主机间进行通信时,要统一采用网络字节顺序。
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(CliendServer),即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。
客户机/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户机/服务器模式的TCP/IP.
一、套接字的类型
1.流式套接字(SOCK_STREAM)
提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。流式套接字实际上是基于TCP协议实现的。
2.数据报式套接字(SOCK_DGRAM )
提供无连接服务。数据包以独立包形式发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。数据报式套接字实际上是基于UDP协议实现的。
3.原始套接字(SOCK_RAW)
二、基于TCP(面向连接)的socket编程
服务器端程序流程如下:
1.创建套接字(socket)。
2.将套接字绑定到一个本地地址和端口上(bind)。
3.将套接字设为监听模式,准备接收客户请求(listen)。
4.等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
5.用返回的套接字和客户端进行通信(send/recv)。
6.返回,等待另一客户请求。
7.关闭套接字。
基于TCP(面向连接)的socket编程的客户端程序流程如下:
1.创建套接字(socket)。
2.向服务器发出连接请求(connect)。
3.和服务器端进行通信(send/recv )。
4.关闭套接字。
在服务器端,当调用accept函数时,程序就会等待,等待客户端调用Connect函数发出连接请求,然后服务器端接受该请求,于是双方就建立了连接。之后,服务器端和客户端就可以利用send和recv函数进行通信了。读者应注意,在客户端并不需要调用bind函数。因为服务器需要接收客户端的请求,所以必须告诉本地主机它打算在哪个IP地址和哪个端口上等待客户请求,因此必须调用bind函数来实现这一功能。而对客户端来说,当它发起连接请求,服务器端接受该请求后,在服务器端就保存了该客户端的IP地址和端口的信息。这样,对服务器端来说,一旦建立连接之后,实际上它己经保存了客户端的IP地址和端口号的信息,因此就可以利用所返回的套接字调用send/recv函数与客户端进行通信。
三、基于UDP(面向无连接)的socket编程
服务器端也叫接收端,对于基于UDP(面向无连接)的套接字编程来说,它的服务器端和客户端这种概念不是很强化,我们也可以把服务器端,即先启动的一端称为接收端,发送数据的一端称为发送端,也称为客户端。
我们先看一下接收端程序的编写:
1.创建套接字(socket )。
2.将套接字绑定到一个本地地址和端口上(bind )。
3.等待接收数据(recvfrom )。
4.关闭套接字。
对于基于UDP的套接字编程,为什么仍然需要调用bind函数进行绑定呢?应注意,虽然面向无连接的socket编程无须建立连接,但是为了完成这次通信,对于接收端来说,它必须先启动以接收客户端发送的数据,因此接收端必须告诉主机它是在哪个地址和端口上等待数据的到来,也就是说,接收端(服务器端)必须调用bind函数将套接字绑定到一个本地地址和端口上。
对于客户端程序的编写非常简单:
1.创建套接字(socket)。
2.向服务器发送数据(sendto)。
3.关闭套接字。
注意,在基于UDP的套接字编程时,利用的是sendto和recvfrom这两个函数实现数据的发送和接收,而基于TCP的套接字编程时,发送数据是调用send函数,接收数据调用recv函数。
套接字表示了通信的端点。我们利用套接字进行通信与利用电话机进行通信是一样的,套接字相当于电话机,IP地址相当于总机号码,而端口号则相当于分机。
Win32Tcp:
#include <Winsock2.h>
#include <stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return;
}
SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
listen(sockSrv,5);
SOCKADDR_IN addrClient;
int len=sizeof(SOCKADDR);
while(1)
{
SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);
char sendBuf[100];
sprintf_s(sendBuf,"Welcome %s to http://www.sunxin.org",
inet_ntoa(addrClient.sin_addr));
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[100];
recv(sockConn,recvBuf,100,0);
printf("%s\n",recvBuf);
closesocket(sockConn);
}
}
Win32Client:
#include <Winsock2.h>
#include <stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 )
{
WSACleanup( );
return;
}
SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(6000);
connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
char recvBuf[100];
recv(sockClient,recvBuf,100,0);
printf("%s\n",recvBuf);
send(sockClient,"This is lisi",strlen("This is lisi")+1,0);
closesocket(sockClient);
WSACleanup();
}
UDPSrv:
#include <WinSock2.h>
#include <stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err = 0;
wVersionRequested = MAKEWORD(2,2);
err = WSAStartup(wVersionRequested,&wsaData);
if (0 != err)
{
return;
}
if (2 != LOBYTE(wsaData.wVersion) || 2 != HIBYTE(wsaData.wVersion))
{
WSACleanup();
return;
}
SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSvr;
addrSvr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSvr.sin_family = AF_INET;
addrSvr.sin_port = htons(6000);
bind(sockSrv,(SOCKADDR*)&addrSvr,sizeof(SOCKADDR));
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
char recvBuf[100] = "";
recvfrom(sockSrv,recvBuf,sizeof(recvBuf),0,(SOCKADDR*)&addrClient,&len);
printf(recvBuf);
closesocket(sockSrv);
WSACleanup();
}
UDPClient:
#include <WinSock2.h>
#include <stdio.h>
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int nErr = 0;
wVersionRequested = MAKEWORD(2,2);
nErr = WSAStartup(wVersionRequested,&wsaData);
if (0 != nErr)
{
return;
}
if (2 != LOBYTE(wsaData.wVersion) || 2 != HIBYTE(wsaData.wVersion))
{
WSACleanup();
return;
}
SOCKET sockClient = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN sockSrc;
sockSrc.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sockSrc.sin_family = AF_INET;
sockSrc.sin_port = htons(6000);
sendto(sockClient,"Hello",strlen("Hello")+1,0,(SOCKADDR*)&sockSrc,sizeof(sockSrc));
closesocket(sockClient);
WSACleanup();
}