协议 | 连接 | 可靠 | 服务 |
---|---|---|---|
TCP | 面向连接的 | 可靠的 | 字节流服务 |
UDP | 无连接 | 不可靠的 | 数据报服务 |
IP | 无连接 | 不可靠的 | 无状态的 |
RUDP:介于TCP和UDP中间,两者取其中。
TCP与UDP通信过程建立的区别。除了它们通信过程建立的不同之外,两者还有以下区别:
1)可靠性;
通信双方均就位,一方发送数据,另一方收到后会做出回应,如果超时未发送成功,会自动重发,数据不会丢失。
2)顺序性;
既然数据是按顺序走在建立的一条隧道中,那么数据遵循“先走先达到”的规则,并且隧道中的数据以“流”的形式传输,发送方发送的前后两次数据之间没有边界,需要接收方自己根据事先规定好的“协议”去判断数据边界。
3)高损耗。
“高损耗”包括机器性能损耗高、宽带流量损耗高。因为通信双方时刻需要维持着连接的存在,这必然会损耗通信双方主机性能,要想维持隧道的通畅,通信双方必须不断地发送检测包和应答包,同时,它还支持数据重发等数据纠错功能,这些都将导致网络流量的增加。
1)不可靠性;
既然无连接,发送方只管发送数据,而不管对方是否能够正确地接收到数据,更不负责数据超时重发等功能。
2)无序性;
数据以“数据报”的形式发送,可以把“数据报”看成是一个“包”。如果把TCP传输数据比如成“河里的流水”,那么UDP传输数据就是‘邮局寄信’。发送方先发送的数据可能后到达,后发送的数据可能先到达,这个跟短消息类似。
3)低损耗。
“低损耗”包括机器性能损耗低、宽带流量损耗低。UDP通信不需要维持一个连接的存在,所以它不需要消耗额外的机器性能。同时它也没有像TCP通信那样为了保持隧道的通畅,而必须不停地发送检测包和应答包,更不会进行一些数据检测纠错、重发等行为。
这次我们只讨论TCP通信。
实现TCP需要的头文件:
#include
#include
#include
#include
需要的函数原型:
① int socket(int domain, int type, int protocol);
//创建socket
② int bind(int sockfd, struct sockaddr *seraddr, socklen_t);
为了了解这个函数我们还需要知道:
TCP如何标识一台主机?
需要知道 IP地址 + 端口号
struct sockaddr_in
{
sa_family_t sin_family;//地址族
u_int16_t sin_port;//端口号
struct in_addr sin_addr;IP地址
}
struct in_addr
{
u_int32_t s_addr;//无符号32位整型值
}
端口号取值范围:0-65535 但0-1024(无法使用,系统预留) 1024-5000保留
一般我们用5000以上端口号
大端模式:高位存低地址
小端模式:高位存高地址
网络字节序:
都用的大端模式(如果主机是小端模式不经过转化在网络中传输数据就会乱套)
③ int listen(int sockfd, int size);
//给sockfd启动监听
④ int accept(int sockfd, struct sockaddr* cliaddr, int len);
⑤ int recv(int c, void *buff ,int buffsize, int flag);
//获取数据
⑥int send(int c, void *buff,int datasize, int flag);
发送数据
服务器端编码流程:
需要的函数原型:
① int socket(int domain, int type, int protocol);//与前面一样
② int connect(int sockfd, (struct sockaddr*)seraddr, int len);//发起连接
③ int send(int c, void *buff,int datasize, int flag);//发起连接
④ int recv(int c, void *buff ,int buffsize, int flag);//获取数据
⑤ close();//发送数据
客户端编码流程:
1.socker();创建套接字
2.connect();发起连接和服务器进行连接
3.while(1)//多次接受发送数据
{
send()/recv();多次
}
4.close();
从创建socket,到建立连接接收数据,最后关闭socket的过程如上图所示。
其中,和建立连接有关系的socket api主要是:connect、bind、listen和accept
为了探究建立连接时发生了什么,和TCP三次握手有什么关系,我们使用之前实验所写的hello/hi程序,用gdb为这四个函数打上断点,并使用wireshark监视相应端口,抓取数据包
当服务端运行bind,listen后,并没有捕获到任何数据包
直到客户端运行connect后,才捕获到TCP三次握手发送的数据包,如下图所示
可以通过抓取的数据包信息看到Socket是如何建立TCP连接的
通过这个实践可以推测,TCP的三次握手是在connect和accept之间完成的,bind和listen只是完成绑定和监听的功能
在上一个实验探究Socket底层是如何实现多态机制的时候,我们发现socket结构体中有一个名为ops
的结构体指针,结构体中又通过函数指针绑定了具体的底层函数,完成了connect
、accept
的实现。在struct proto tcp_prot
的初始化中我们可以找到对应的绑定函数。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
...
};
可以看到,socket->ops->connect
绑定了函数tcp_v4_connect
,socket->ops->accept
绑定了inet_csk_accept
...
//设置套接字状态,从CLOSE变为TCP_SYN_SENT,对应客户端从CLOSED->SYN_SENT这一过程
tcp_set_state(sk, TCP_SYN_SENT);
//将套接字sk放入TCP连接管理哈希链表中
err = inet_hash_connect(&tcp_death_row, sk);
if (err)
goto failure;
//为连接分配一个随机的空闲端口
err = ip_route_newports(&rt, IPPROTO_TCP,
inet->inet_sport, inet->inet_dport, sk);
if (err)
goto failure;
...
...
if (!tp->write_seq)
//初始化报文内容
tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
inet->inet_id = tp->write_seq ^ jiffies;
//构建并发送SYN数据报
err = tcp_connect(sk);
rt = NULL;
if (err)
goto failure;
...
对inet_csk_accept
的部分源码分析
在分析代码前我们需要了解,套接字有监听套接字和具体通信的套接字(accept
返回的那个)。监听套接字的扩展结构inet_connection_sock
中存在icsk_accept_queue
成员,此成员中有两个队列,一个用于完全建立连接(完成三次握手)的队列,此队列项中会包含新建的
用于通信的sock结构,在进程不在阻塞获得此sock结构后会把此队列项从完全建立连接的队列删除.此队列的最大长度即是listen(int s, int backlog)
中第二个参数指定的;另一个队列是半连接队列,即还没有完成三次握手的队列项会加入到此队列,此队列项中的sock完成三次握手后会从此队列中移除,添加到完全建立连接的队列中
...
//检查套接字是否处于监听状态(应该是在调用listen时设置的)
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
//在监听套接字上的连接队列如果为空(没有任何连接完成)
if (reqsk_queue_empty(&icsk->icsk_accept_queue)) {
//设置接收超时时间,若调用accept的时候设置了O_NONBLOCK,表示马上返回不阻塞进程
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
error = -EAGAIN;
if (!timeo)//如果是非阻塞模式timeo为0 则马上返回
goto out_err;
//将进程阻塞,等待连接的完成,inet_csk_wait_for_connect核心是一个循环,等待三次握手中,客户端发来的最后一个ACK报文
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
//在监听套接字建立连接的队列中删除此request_sock连接项 并返回建立连接的sock
newsk = reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
//套接字状态变为TCP_SYN_RECV,对应连接建立完成,服务端进入ESTABLISHED状态
WARN_ON(newsk->sk_state == TCP_SYN_RECV)
分析这两段代码后,我们对TCP连接的建立已经有了一部分认知,tcp_v4_connect()
会发送SYN报文开始三次握手,而inet_csk_accept
接收来自客户端的ACK报文,标志着TCP连接建立完成。
三次握手的分析还并不完整,服务器端是如何接收第一次握手发来的SYN数据报,并返回SYN+ACK
数据报的?实际上服务器端接收到SYN报文后,最终会调用tcp_v4_do_rcv()
进行处理, 和tcp_send_ack()
一起返回第二次握手中的SYN+ACK
报文,客户端则是使用tcp_send_ack()
返回最后的ACK报文。受限于篇幅,不再对这些函数的源码进行分析