一个Server-Client模型程序的开发原理:
服务器,使用ServerSocket监听指定的端口,端口可以随意指定(由于1024以下的端口通常属于保留端口,在一些操作系统中不可以随意使用,所以建议使用大于1024的端口),等待客户连接请求,客户连接后,会话产生;在完成会话后,关闭连接。
客户端,使用Socket对网络上某一个服务器的某一个端口发出连接请求,一旦连接成功,打开会话;会话完成后,关闭Socket。客户端不需要指定打开的端口,通常临时的、动态的分配一个1024以上的端口。
Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。 Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 Socket建立为了建立Socket,程序可以调用Socket函数,该函数返回一个类似于文件描述符的句柄。socket函数原型为:int socket(int domain, int type, int protocol);domain指明所使用的协议族,通常为PF_INET,表示互联网协议族(TCP/IP协议族);type参数指定socket的类型:SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值"0"。Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。 Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间。 Socket执行体为你管理描述符表。两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。 socket在测量软件中的使用也很广泛
socket深层次理解
Socket编程基本就是listen,accept以及send,write等几个基本的操作。
对于网络编程,我们也言必称TCP/IP,似乎其它网络协议已经不存在了。对于TCP/IP,我们还知道TCP和UDP,前者可以保证数据的正确和可靠性,后者则允许数据丢失。最后,我们还知道,在建立连接前,必须知道对方的IP地址和端口号。除此,普通的程序员就不会知道太多了,很多时候这些知识已经够用了。最多,写服务程序的时候,会使用多线程来处理并发访问。
我们还知道如下几个事实:
1。一个指定的端口号不能被多个程序共用。比如,如果IIS占用了80端口,那么Apache就不能也用80端口了。
2。很多防火墙只允许特定目标端口的数据包通过。
3。服务程序在listen某个端口并accept某个连接请求后,会生成一个新的socket来对该请求进行处理。
于是,一个困惑了我很久的问题就产生了。如果一个socket创建后并与80端口绑定后,是否就意味着该socket占用了80端口呢?如果是这样的,那么当其accept一个请求后,生成的新的socket到底使用的是什么端口呢(我一直以为系统会默认给其分配一个空闲的端口号)?如果是一个空闲的端口,那一定不是80端口了,于是以后的TCP数据包的目标端口就不是80了--防火墙一定会组织其通过的!实际上,我们可以看到,防火墙并没有阻止这样的连接,而且这是最常见的连接请求和处理方式。我的不解就是,为什么防火墙没有阻止这样的连接?它是如何判定那条连接是因为connet80端口而生成的?是不是TCP数据包里有什么特别的标志?或者防火墙记住了什么东西?
后来,我又仔细研读了TCP/IP的协议栈的原理,对很多概念有了更深刻的认识。比如,在TCP和UDP同属于传输层,共同架设在IP层(网络层)之上。而IP层主要负责的是在节点之间(End to End)的数据包传送,这里的节点是一台网络设备,比如计算机。因为IP层只负责把数据送到节点,而不能区分上面的不同应用,所以TCP和UDP协议在其基础上加入了端口的信息,端口于是标识的是一个节点上的一个应用。除了增加端口信息,UPD协议基本就没有对IP层的数据进行任何的处理了。而TCP协议还加入了更加复杂的传输控制,比如滑动的数据发送窗口(Slice Window),以及接收确认和重发机制,以达到数据的可靠传送。不管应用层看到的是怎样一个稳定的TCP数据流,下面传送的都是一个个的IP数据包,需要由TCP协议来进行数据重组。
所以,我有理由怀疑,防火墙并没有足够的信息判断TCP数据包的更多信息,除了IP地址和端口号。而且,我们也看到,所谓的端口,是为了区分不同的应用的,以在不同的IP包来到的时候能够正确转发。
TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口,这就是Socket编程接口--原来是这么回事啊!
在Socket编程接口里,设计者提出了一个很重要的概念,那就是socket。这个socket跟文件句柄很相似,实际上在BSD系统里就是跟文件句柄一样存放在一样的进程句柄表里。这个socket其实是一个序号,表示其在句柄表中的位置。这一点,我们已经见过很多了,比如文件句柄,窗口句柄等等。这些句柄,其实是代表了系统中的某些特定的对象,用于在各种函数中作为参数传入,以对特定的对象进行操作--这其实是C语言的问题,在C++语言里,这个句柄其实就是this指针,实际就是对象指针啦。
现在我们知道,socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。
现在我们明白,如果一个程序创建了一个socket,并让其监听80端口,其实是向TCP/IP协议栈声明了其对80端口的占有。以后,所有目标是80端口的TCP数据包都会转发给该程序(这里的程序,因为使用的是Socket编程接口,所以首先由Socket层来处理)。所谓accept函数,其实抽象的是TCP的连接建立过程。accept函数返回的新socket其实指代的是本次创建的连接,而一个连接是包括两部分信息的,一个是源IP和源端口,另一个是宿IP和宿端口。所以,accept可以产生多个不同的socket,而这些socket里包含的宿IP和宿端口是不变的,变化的只是源IP和源端口。这样的话,这些socket宿端口就可以都是80,而Socket层还是能根据源/宿对来准确地分辨出IP包和socket的归属关系,从而完成对TCP/IP协议的操作封装!而同时,放火墙的对IP包的处理规则也是清晰明了,不存在前面设想的种种复杂的情形。(以上为转载)
例子:/*
服务器端程序:
1、创建socket
2、将套接字绑定到一个本地地址和端口上bind
3、将套接字设为监听模式,准备接受客户请求listen
4、等待客户请求到来;当请求到来后,结合搜此次连接的套接字accept
5、用返回的套接字和客户端进行通信send/recv
6、返回,等待另一客户请求。
7、关闭套接字。
*/
#include "WinSock2.h"
#include "stdio.h"
#pragma comment(lib,"Ws2_32.lib")//导入库,lib表示库," "表示库的名称
int main(int argc, char* argv[])
{
//加载WinSock库
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) return 0;
if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{
WSACleanup( );
return 0;
}
SOCKET lSocket,aSocket; //SOCKET句柄
SOCKADDR_IN AddrSer;
AddrSer.sin_family=AF_INET; //网络类型/指代协议族
AddrSer.sin_port=htons(5000); //端口号,使用网络字节顺序,短整形
//存储IP地址,使用in_addr这个数据结构
AddrSer.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//INADDR_ANY表示接受任何机器的请求 ,长整形
lSocket=socket(AF_INET,SOCK_STREAM,0);
//绑定服务器地址
if(::bind(lSocket,(SOCKADDR*)&AddrSer,sizeof(SOCKADDR))==SOCKET_ERROR)
{
printf("绑定套接字失败");
return 0;
}
::listen(lSocket,1);//开始监听客户端的连接请求
SOCKADDR_IN AddrCli;//客户端的地址信息
int len=sizeof(SOCKADDR);
aSocket=::accept(lSocket,(SOCKADDR*)&AddrCli,&len);//连接成功时,产生新的socket用来与客户端通信
char recBuf[1024];
char senBuf[1024];
while (1)
{
//TCP下为recv与send,UDP下为recefrom与sendto
::recv(aSocket,recBuf,1024,0);
if (recBuf[0]=='#')
{
printf("客户端请求断开\n");
printf("正在断开...");
Sleep(3000);
break;
}
printf("客户端:%s\n",recBuf);
printf("服务器:");
gets(senBuf);//warming:gets()is unsafe
if (senBuf[0]=='#')
{
::send(aSocket,senBuf,1024,0);
printf("正在请求断开...\n");
Sleep(3000);
return 0;
}
//printf("服务器:%s\n",senBuf);
::send(aSocket,senBuf,1024,0);
memset(recBuf,' ',1024);
memset(senBuf,' ',1024);
}
::closesocket(aSocket);
::closesocket(lSocket);
::WSACleanup();
return 0;
}
/*
客户端程序:
1、创建套接字socket
2、想服务器发出连接请求connect
3、和服务器进行通信send/recv
4、关闭套接字。
*/
//#include "stdafx.h"
#include "WinSock2.h"
#include "stdio.h"
#pragma comment(lib,"Ws2_32.lib")
int main(int argc, char* argv[])
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) return 0;
if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{
WSACleanup( );
return 0;
}
SOCKET cSocket;
cSocket=socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN addrSer;
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(5000);
addrSer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
if(::connect(cSocket,(SOCKADDR*)&addrSer,sizeof(SOCKADDR))==SOCKET_ERROR)
{
printf("连接服务器失败");
return 0;
}
char senBuf[1024];
char recBuf[1024];
while(1)
{
printf("客户端:");
gets(senBuf);
if (senBuf[0]=='#')
{
::send(cSocket,senBuf,1024,0);
printf("正在请求断开...\n");
Sleep(3000);
return 0;
}
//printf("客户端:%s\n",senBuf);
::send(cSocket,senBuf,1024,0);
::recv(cSocket,recBuf,1024,0);
if(recBuf[0]=='#')
{
printf("与服务器断开");
printf("正在断开...");
Sleep(3000);
break;
}
printf("服务器:%s\n",recBuf);
memset(recBuf,' ',1024);
memset(senBuf,' ',1024);
}
::closesocket(cSocket);
::WSACleanup();
return 0;
}