📄 socket始解.txt
字号:
一、socket知识介绍
1. 阻断型和非阻断型端口.
阻断型*(stThreadBlocking)*Socket在运行中,应用程序调用了Socket接口函数后,
函数并不马上返回,一直要到相应的操作完成才返回,这期间应用程序处于等待状
态.这种运行方式适合需要同步通信的应用程序.
非阻断型*(stNonBlocking)*Socket在运行中,Socket接口函数调用后立即返回应用
程序可以进行别处的工作,但此时相应的Socket操作并不一定完成.当操作确实完成
时,应用程序可以收到相应的消息,根据收到的消息再进行处理.这种运行方式适合
异步通信应用程序.
相较而言,非阻断型Socket效率比较高,但编程比较复杂,并要求有相应系统软件的
支持.而阻断型Socket编程比较简单,效率低一些.大多数情况下都应该用非阻断型
Socket.
2. 端口交互图:
图3-2TCP协议,
3. WINSOCK C/S的建立过程:
客户/服务器模式过程中采取的是客户主动请求方式:
首先服务器方要先启动,并根据请求提供相应服务:
1. 打开一通信通道并告知本地主机,它愿意在某一公认端口上,如FTP为21,接收
客户请求;
2. 等待客户请求到达该端口;
3. 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要
激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此
客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通
信链路,并终止。
4. 返回第二步,等待另一客户请求。
5. 关闭服务器
客户方:
1. 打开一通信通道,并连接到服务器所在主机的特定端口;
2. 向服务器发服务请求报文,等待并接收应答;继续提出请求......
3. 请求结束后关闭通信通道并终止。
服务器 客户端
________________________________________________
1 初始化WinSock Dll库 1 初始化WinSock Dll库
____________________________________________________
2 创建一个SOCKET 2 创建一个SOCKET
_____________________________________________________
3 绑定SOCKET
_____________________________________________________
4 监听端口 3 客户端连接服务器
_____________________________________________________
5 接受一个连接
______________________________________________________-
6 发送和接受数据 4 发送和接受数据
___________________________________________________
7 关闭连接 5 关闭连接
__________________________________________________
二、Socket建立过程详细描述
1. 加载WinSock Dll
int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
wVersionRequested:指定用到的WinSock的最低版本,高字节指定副版本,低字节
指定主版本。
lpWSAData:结构类型,系统把加载进去的版本信息添加到这个结构中。
当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相
应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调
用所请求的Socket库中的其它Socket函数了。该函数执行成功后返回0,不成功则
返回几个错误代码之一。这个函数和WSACleanup对应。
例:假如一个程序要使用2.1版本的Socket,那么程序代码如下
wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
2. 创建一个Socket
SOCKET socket (
int af,
int type,
int protocol
);
af:通信协议的协议族,Internet协议族的标志是PF_INET/AF_INET
type:协议类型志:
SOCK_STREAM ------面向连接的字节流类型,
SOCK_DGRAM ------面向无连接的数据报类型。
SOCK_RAW------原始IPv4、IPv6
SOCK_PACKET------ LINUX上类似BPF的直接访问底层数据链路的套接口类型
SOCK_RDM------可靠传递消息
SOCK_SEQPACKET------有序分组
protocol: Socket采用的协议类型
IPPROTO_UDP UDP协议 用于无连接数据报套接字
IPPROTO_TCP TCP协议 用于流式套接字
IPPROTO_ICMP ICMP协议用于原始套接字
0为给定fa和type组合的系统默认值
该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回
INVALID_SOCKET。
套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符
表,该表中存放着套接字描述符和套接字数据结构的对应关系,因此根据套接字描
述符就可以找到其对应的套接字数据结构。
下面是一个创建流套接字的例子:
SOCKET ListenSocket=socket(PF_INET,SOCK_STREAM,0);
3. 绑定Socket:
当创建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。
服务程序必须调用bind函数来给其绑定一个IP地址和一个特定的端口号。
客户程序一般不必调用bind函数来为其Socket绑定IP地址和端口号。
int bind (
socket s,
const struct sockaddr FAR* name,
int namelen
);
s:一个socket,由socket()函数返回。
Name:sockaddr结构的变量
Namelen:Name结构的长度。
如果函数调用成功,将返回0,如果不成功,将返回SOCKET_ERROR
sockaddr在Winsock中有两种结构,它们是:
struct sockaddr {
u_short sa_family; /* 地址族, 一般为AF_INET */
char sa_data[14]; /* 14 字节的协议地址,包含该IP地址和端口
号 */
};
struct sockaddr_in {
short sin_family; /* 地址族 */
u_short sin_port; /* 端口号,若为0,则系统随机选择一个未被使用的
端口号 */
struct in_addr sin_addr; /*32位的IP地址 */
char sin_zero[8]; /*保留值,*/
};
sin_addr:结构体中只有一个唯一的字段s_addr,表示IP地址,该字段是一个整
数,一般用函数inet_addr()把字符串形式的IP地址转换成unsigned long型的整
数值后再赋值给s_addr。
sin_zero:用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用
bzero()或memset()函数将其置为零。指向sockaddr_in 的指针和指向sockaddr的
指针可以相互转换。
有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序
在为其Socket绑定IP地址时可以把htonl(INADDR_ANY)置给s_addr,这样做的好处
是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上
的服务程序的Socket绑定一个固定的IP地址,那么就只有与该IP地址处于同一个网
段上的客户程序才能与该服务程序通信。
下面是一个bind函数调用的例子:
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));
4. 服务端监听端口
在Bind之后,调用下面函数进行监视。
int listen (
SOCKET s,
int backlog
);
s:一个socket,由socket()函数返回
Backlog:进入队列中允许的连接的个数,进入的连接请求在使用系统调用accept
()应答之前要在进入队列中等待。这个值是队列中最多可以拥有的请求的个数。如
果值设为SOMAXCONN,将由Socket的服务提供商设定一个合理的值。
函数成功时返回0,否则返回SOCKET_ERROR
5. 客户端连接服务端
int connect (
SOCKET s,
const struct sockaddr FAR* name,
int namelen
);
函数里的参数和bind()一样
客户端调用connect函数来使客户Socket s与服务Socket进行连接。如果连接成
功,connect返回0;如果失败则返回SOCKET_ERROR。
下面是一个例子:
struct sockaddr_in daddr;
memset((void *)&daddr,0,sizeof(daddr));
daddr.sin_family=AF_INET;
daddr.sin_port=htons(8888);
daddr.sin_addr.s_addr=inet_addr("133.197.22.4");
connect(ClientSocket,(struct sockaddr *)&daddr,sizeof(daddr));
6. 接受一个连接
当客户端连接服务端,服务端调用下面函数接收客户的请求,并向客户机发送应答信息
SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
Addr:保存客户端信息的指针
Addrlen:addr的长度,一般是Sizeof(addr)
服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出
排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通
道,原来的那个Socket继续监视其他客户端的连接。如果连接成功,就返回新创建
的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就
返回INVALID_SOCKET
7. 发送和接受数据
int send (SOCKET s,const char FAR * buf, int len,int flags );在面向连接
的情况下发送数据
int recv (SOCKET s, char FAR* buf, int len, int flags );在面向连接的情
况下接收数据,另外还有sendto和recvfrom用于无连接情况,
Buf:发送或接收的缓冲区,
Len:缓冲区的大小,
Flags:网络呼叫产生方式标志,值可以为0,MSG_DONTROUTE或 MSG_OOB。
当调用send函数时,
send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,
如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s
的发送缓冲中的数据,
如果是就等待协议把数据发送完,
如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,
那么send就比较s的发送缓冲区的剩余空间和len,
如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发
送完,
如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里
(注意 并不是send把s的发送缓冲中的数据传到连接的另一端的,而
是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
如果send函数copy数据成功,就返回实际copy的字节数,
如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
如果send在等待协议传送数据时网络断开的话,那么send函数也返回
SOCKET_ERROR。要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间
里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。
如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回
SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字
的发送缓冲中的数据被协议传送完毕才能继续,
如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR)
8. 关闭连接
通信完毕后,需要关闭Socket,
函数声明如下:int closesocket (SOCKET s );
closesocket函数用来关闭一个描述符为s套接字。由于每个进程中都有一个套接字
描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字
数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数
据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向
该结构。当调用closesocket函数时,操作系统先检查套接字数据结构中的该字段
的值,如果为1,就表明只有一个套接字描述符指向它,因此操作系统就先把s在套
接字描述符表中对应的那条表项清除,并且释放s对应的套接字数据结构;如果该
字段大于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把s对
应的套接字数据结构的引用次数减1。
在程序的最后,需要调用下面的函数,结束WinSock DLL
int WSACleanup (void);
函数成功时返回0,否则返回SOCKET_ERROR。
三、TServerSockett和TClientSocket的源代码
1. 几个类的关系:
以ServerSocket和ClientSocket的一次操作流程来跟踪它们的源代码,看看他们是
怎么样封WinSocket的。以非阻塞方式来例子。
2. ServerSocket分析
2.1.ServerSocket构造函数:
constructor TServerSocket.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
//继承自TComponent的构造函数
FServerSocket := TServerWinSocket.Create(INVALID_SOCKET);
//创建一个TServerWinSocket的对象,这个才是真正封装SocketApi的类
InitSocket(FServerSocket);
//这是ServerSocket的祖先类TAbstractSocket的一个方法,传入参数是成员
FserverSocket,完成的功能是将ServerSocket的事件指针指向TServerWinSocket
的事件,使其能处理Socket触发的事件.
end;
看FServerSocket := TServerWinSocket.Create(INVALID_SOCKET);做了什么:
constructor TServerWinSocket.Create(ASocket: TSocket);
begin
FConnections := TList.Create;
FActiveThreads := TList.Create;
FListLock := TCriticalSection.Create;
inherited Create(ASocket);
FAsyncStyles := [asAccept];
end;
首先创建两个TList对象,一个是FConnections,代表各个处理客户连接的
Socket,它对应于属性:property Connections[Index: Integer]:
TCustomWinSocket,你可以通过这个属性对各个客户连接进行操作。
FActiveThreads 管理由Connections 数组确定的的客户端连接线程
TServerClientThread,它对应的属性是ActiveThreads,这个是只读属性,返回当
前正在使用的TServerClientThread对象的个数。接下来创建互斥量对象,用于线
程同步的处理。
到这里又调用其父类的构造函数,传递的参数就是ServerSocket给的值
INVALID_SOCKET
好,再继续跟踪,到其父类的构造函数去看一下,我们这时应该保留一个问题,按
照WinSock的编程模式,刚开始应该是初始化Winsock.DLL,并调用绑定监听函数,
这些API是什么在地方被调用呢?
constructor TCustomWinSocket.Create(ASocket: TSocket);
begin
inherited Create;
Startup;
FSocketLock := TCriticalSection.Create;
FASyncStyles := [asRead, asWrite, asConnect, asClose];
FSocket := ASocket;
FAddr.sin_family := PF_INET;
FAddr.sin_addr.s_addr := INADDR_ANY;
FAddr.sin_port := 0;
FConnected := FSocket <> INVALID_SOCKET;
end;
首先调用TObject的构造函数,再调用Sartup;分析完这个函数再看里面的代码,这
里可以猜测它里面会调用WSAStartup函数。
接下来看到从ServerSocket传过来的参数指定给了TCustomWinSocket的成员,还有
下面几个成员的设置,可以肯定,这里是对Socket进行初始化,结合开头所讲的知
识,再看看源代码。应该不难理解了吧。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -