一.TCP的三次握手
建立一个TCP连接会发生下述情形:
(1) 服务器必须准备好接受外来的连接,这通常通过调用socket、bind和listen这三个函数来完成,我们称之为被动打开(passtive open)
(2) 客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立)连接中发送的数据的初始序列号(J),通常SYN分节不带数据,其所在的IP数据报只含有一个IP首部,一个TCP首部及可能有的TCP选项
(3) 服务器必须确认(ACK)客户的SYN,同时自己也得发一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号(K),服务器在单个分节中发送SYN和对客户SYN的ACK(确认)
(4) 客户必须确认服务器的SYN
这种交换至少需要三个分组,因此称之为TCP的三次握手.1>客户端发SYN J到服务器2>服务器发SYN K, ACK J+1到客户端3>客户端回ACK K+1到服务器,第2>步服务器既要确定(ACK)它收到了客户端的消息,又要发一个SYN询问客户端是否准备好进行数据通讯了,两部分做一个发送.(个人理解),ACK带的总是对方序号+1
二 .socket函数
SOCKET socket( IN int af,//指明了协议族 IN int type, IN int protocol );
返回值:成功返回非负的描述符(sockfd),失败返回INVALID_SOCKET,如失败可以通过WSAGetLastError得到指定的错误码.
af:指明了协议族
family |
说明 |
AF_INET |
IPv4协议 |
AF_INET6 |
IPv6协议 |
AF_LOCAL |
Unix域协议 |
AF_ROUTE |
路由套接字 |
AF_KEY |
密钥套接字 |
TCP/UDP一般使用AF_INET就行了,尝试了下其他协议族,在TCP/UDP下,
AF_INET6返回10047(使用了与请求的协议不兼容的地址)
Type:套接字类型
type |
说明 |
SOCK_STREAM |
字节流套接字 |
SOCK_DGRAM |
数据报套接字 |
SOCK_SEQPACKET |
有序分组套接字 |
SOCK_RAW |
原始套接字 |
TCP是一个字节流协方,仅支持SOCK_STREAM套接字
UDP仅支持SOCK_DGRAM套接字
Protocol:
protocol |
说明 |
IPPROTO_TCP |
TCP传输协议 |
IPPORTO_UDP |
UDP传输协议 |
IPPORTO_SCTP |
SCTP传输协议 |
如果指定为0,系统会根据地址格式和套接字类别,自动选择一个合适的协议,推荐这种方法。
三. SOCKADDR_IN和bind函数
struct sockaddr_in { short sin_family; USHORT sin_port; IN_ADDR sin_addr; CHAR sin_zero[8]; } SOCKADDR_IN;
MSDN解释说此结构用于IPv4,
sin_family:AF_INET(IPv4协议族)
sin_port:将要分配给套接字的端口
in_addr:套接字主机的IP地址,设为INADDR_ANY表示通配地址,因为IPv4的IP地址是一个32位的值,所以可以直接用htonl(INADDR_ANY)赋值
通配地址应该是指任何分配给主机的IP地址,一般每个机器只有一个IP地址,但有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址。
创建套接字后,需bind把该套接字绑定到本地的某个地址和端口上
int bind(
SOCKET s,
const struct sockaddr FAR * name,
int namelen
);
返回值:成功返回0,失败返回SOCKET_ERROR,通过WSAGetLastError得到指定的错误码.常见错误为EADDRINUSE(地址已使用)
Name:指向特定于协议的地址结构的指针,基于TCP,IPv4可以用SOCKADDR_IN代替,IPv6可以用SOCKADDR_IN6代替
Namelen:name的长度
对于TCP,调用bind可以指定一个端口号或指定一个IP地址,可都指定,可指定其中一个,可全都不指定
如果一个TCP客户或服务器未曾调用bind捆绑端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口,让内核选择临时端口对TCP客户是正常的,然而对服务器是罕见的,因为服务器是通过它们的众所周知的端口被大家认识的。
如果绑定一个特定IP地址,这个IP地址必须属于其所在主机的网络接口之一,对于TCP客户端,这就为在该套接字上发送的IP数据报指定了源IP地址,对于TCP服务器,这就限定该套接字只接收那个目的地址为这个IP地址的客户连接,所以,TCP客户通常不捆绑IP地址,当连接套接字时,内核将根据所用的外出网络接口自动选择源IP地址,所用的外出网络接口取决于到达服务器所需的路径,如果TCP服务器没有捆绑IP地址,内核就把客户发送的SYN的目的IP做为服务器的源IP地址。(getsockname可以获取来自客户的目的地址)
只需指定端口号为0,那么内核就在bind被调用时选择一个临时端口,如果指定IP地址为通配地址,那么内核将在套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个IP地址.
四.Listen函数
int listen( SOCKET s, int backlog );
返回值: 成功返回0,失败返回SOCKET_ERROR
MSDN说它把套接字置于一种监听即将到来的连接的状态
Listen函数仅由TCP服务器调用,它做两件事:
(1) 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字,listen函数据一个未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求,设用listen导致套接字从CLOSED状态转换成LISTEN状态
(2) Backlog规定了内核应该为相应套接字排队的最大连接个数
内核为任何一个给定的监听套接字维护两个队列:
(1)未完成连接队列,队列元素为某个客户发出的SYN分节到达服务器,而服务器正在等待完成相应的TCP三次握手过程,这些套接字处于SYN_RCVD状态
(2)已完成连接队列,队列元素为某个已完成TCP三次握手过程的客户,这些套接字处理ESTABLISHED状态。
必须满足:未完成连接队列+已完成连接队列<=Backlog(Berkeley给它设了个模糊因子,所以也可能是Backlog*1.5)
队列形成过程:当来自客户的SYN到达时,TCP在未完成连接队列是创建一个新项,然后响应第二次握手,也就是服务器发SYN K, ACK J+1(见上面的TCP三次握手),这一项一直保留在未完成连接队列中,直到第三次握手到达或超时为止,如果三次握手正常完成,该项被移到已完成队列的队尾,当进程调用accept时,已完成连接队列中的队头项将返回给进程,如果该队列为空,那么进程进入等待,直到TCP在该队列中放入一项才唤醒它
历史上Backlog总是设为5,因为这是4..2BSD支持的最大值,现在可以设定较大的值了,因为就算内核不支持这么大,它也会悄悄的把这个值截成自身能支持的最大值。
当一个客户SYN到达时,若这些队列是满的,TCP就直接忽略该分节(也就是不发RST),因为客户TCP将重发SYN,期望不久能在这些队列中找到可用空间
对RST google了下:
RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误:
(1) 建立连接的SYN到达某端口,但是该端口上没有正在监听的服务
(2) TCP想取消一个已有连接
(3) TCP接收到了一个根本不存在的的连接上的分节
可使用RST攻击
可使用SYN flooding发起攻击,通过伪装的SYN装填未完成连接队列,使合法的SYN排不上队,从而导致针对合法客户的服务被拒绝.
五.accpet函数
SOCKET accept( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen );
返回值:成功返回非负描述符,失败返回INVALID_SOCKET
Accpet函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接为空,那么进程处于等待状态(假定套接字为阻塞方式)
addr:是out型的,用来返回已连接的对端进程(客户)的协议地址,
addrlen:调用前,置为Addr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数
accept的第一个参数为监听套接字,而如果成功,返回值为一个已连接套接字,一个服务器通常仅创建一个监听套接字,它在该服务器的生命期一直存在,内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是通过了TCP三次握手的客户),当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭了(手动调用closesocket关掉已处理完的已连接套接字)
六.recvfrom和sendto函数
UDP套接字状态下,客户不与服务器建立连接,只管sendto向服务器发送数据,其中必须指定目的地,同样,服务器不接受来自客户的连接,而只管调用recvfrom等待某个客户的数据的到来,recvfrom将与所接收的数据报一同返回客户的协议地址,因此服务器可以把响应发给正确的客户。
int recvfrom( SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen );
返回值:成功返回0,失败返回SOCKET_ERROR
类似read函数,不过多了三个额外的参数
S:准备接收数据的套接字
buf:读入缓冲区的指针
len: buf的大小
flags:一般置为0
from:接收发送方(客户)的地址信息
fromlen:调用前,置为from所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数
七.getsockopt和setsockopt
(参看UNIX网络编程第7章)
int setsockopt( SOCKET s, int level, int optname, const char * optval, int optlen ); int getsockopt( SOCKET s, int level, int optname, char * optval, int * optlen );
返回值:成功返回0,失败返回SOCKET_ERROR
s:被查询或被设置的套接字
level:,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET
SOL_SOCKET |
套接字层 |
IPPROTO_IP |
IPV4层 |
IPPROTO_ICMPV6 |
|
IPPROTO_IPV6 |
IPV6层 |
IPPROTO_IP/IPPROTO_IPV6 |
|
IPPROTO_TCP |
TCP层 |
IPPROTO_SCTP |
SCTP层 |
optname: 参看UNIX网络编程第7章,自己程序用到了SO_BROADCAST设置
套接字选项分为两大类:
(1) 启用或禁止某个特性的二元选项(标志选项)
(2) 取得并返回我们可以设置或检查的特定值的选项
调用getsockopt时,optval是一个整数,它返回的值为0表示相应的选项被禁止了,不为0表示相应选项被启用,类似的,setsockopt函数需要一个不为0的optval来启用选项,一个为0的optval来禁止选项.