TCP与UDP协议与应用
TCP
TCP段结构
传输控制协议接受来自数据流的数据,将其分成块,并添加创建TCP段的TCP头。然后将 TCP段封装到 Internet 协议 (IP) 数据报中,并与对等方进行交换。
术语 TCP 数据包出现在非正式和正式用法中,而在更精确的术语中,段指的是TCP协议数据单元 (PDU),数据报指的是 IP PDU,帧指的是数据链路层 PDU:
进程通过调用 TCP 并将数据缓冲区作为参数传递来传输数据。 TCP 将这些缓冲区中的数据打包成段并调用 Internet 模块,将每个段传输到目标 TCP。
TCP段由段头和数据段组成。段头包含10个必要字段和一个可选的扩展字段(可选项)。数据部分跟在段头后面,是为应用程序携带的有效载荷数据。数据段的长度没有在段头中指定;它可以通过从IP报头中指定的IP数据报总长度中减去段报头和IP报头的长度来计算。
- 源端口(16 位),标识发送端口。
- 目标端口(16 位),标识接收端口。
- 序列号(32 位),具有双重作用:如果设置了 SYN 标志 (1),则这是初始序列号。实际第一个数据字节的序列号和相应ACK中的确认号就是这个序列号加1。如果 SYN 标志清除 (0),则这是当前会话的该段的第一个数据字节的累积序列号。
- 确认号(32 位),如果设置了 ACK 标志,则该字段的值是 ACK 的发送者期望的下一个序列号。这确认收到所有先前的字节(如果有)。每一端发送的第一个 ACK 确认另一端的初始序列号本身,但没有数据。
- 数据偏移(4 位)以 32 位字指定TCP段头的大小。段头最小为 5 个字,最大为 15 个字,因此最小为 20 个字节,最大为 60 个字节,段头最多允许 40 个字节的选项。这个字段的名字来源于它也是从 TCP 段的开始到实际数据的偏移量。
- 保留(3 位),为将来使用,应设置为零。
- 标志(9 位),包含 9 个 1 位标志(控制位)
位名称 | 说明 |
---|---|
NS (1 bit) | ECN-nonce - 隐藏保护 |
CWR(1 位) | 拥塞窗口减少 (CWR) 标志由发送主机设置,以指示它收到了一个设置了 ECE 标志的 TCP 段并在拥塞控制机制中做出了响应。 |
ECE(1 位) | ECN-Echo 有双重作用,取决于 SYN 标志的值。如果设置了 SYN 标志 (1),则 TCP 对等方具有 ECN 能力。如果 SYN 标志清除 (0),则表示在正常传输期间接收到 IP 报头中设置了拥塞经历标志 (ECN=11) 的数据包。这用作网络拥塞(或即将发生拥塞)的指示以TCP 发送方。 |
URG(1 位) | 表示 Urgent 指针字段有效 |
ACK(1 位) | 表示确认字段是重要的。客户端发送的初始 SYN 数据包之后的所有数据包都应设置此标志。 |
PSH(1 位) | 推送功能。请求将缓冲的数据推送到接收应用程序。 |
RST(1 位) | 重置连接 |
SYN(1 位) | 同步序列号。只有从每一端发送的第一个数据包才应该设置这个标志。其他一些标志和字段会根据此标志更改含义,有些仅在设置时才有效,有些则在清除时才有效。 |
FIN(1 位) | 来自发送方的最后一个数据包 |
- 窗口大小(16 位), 接收窗口的大小,指定该段的发送方当前愿意接收的窗口大小单位的数量。
- 校验和(16 位), 16 位校验和字段用于TCP报头、有效载荷和IP伪报头的错误检查。伪头由源 IP 地址、目标IP地址、TCP协议的协议号 (6) 以及 TCP 头和有效载荷的长度(以字节为单位)组成。
- 紧急指针(16位), 如果设置了 URG 标志,则该 16 位字段是与指示最后一个紧急数据字节的序列号的偏移量。
- 可选项(可变 0-320 位,以 32 位为单位),该字段的长度由数据偏移字段决定。选项最多有三个字段:Option-Kind(1 字节)、Option-Length(1 字节)、Option-Data(变量)。 Option-Kind 字段指示选项的类型,并且是唯一非可选的字段。根据 Option-Kind 值,可以设置接下来的两个字段。 Option-Length 表示选项的总长度,Option-Data 包含与选项相关的数据(如果适用)。例如,Option-Kind 字节为 1 表示这是一个仅用于填充的无操作选项,并且后面没有 Option-Length 或 Option-Data 字段。一个 Option-Kind 字节为 0 标志着选项的结束,也只有一个字节。 Option-Kind 字节为 2 用于指示最大段大小选项,后面跟着一个 Option-Length 字节,指定 MSS 字段的长度。 Option-Length 是给定选项字段的总长度,包括 Option-Kind 和 Option-Length 字段。因此,虽然 MSS 值通常用两个字节表示,但 Option-Length 将为 4。例如,值为 0x05B4 的 MSS 选项字段在 TCP 选项部分编码为 (0x02 0x04 0x05B4)。
某些选项可能只有在设置了 SYN 时才会发送;它们在下面表示为 [SYN]。 Option-Kind 和标准长度表示为 (Option-Kind, Option-Length)。
链接建立过程
TCP使用三路握手过程建立连接。连接的每个方向上的数据流都是独立控制的,以避免与初始序列号产生歧义。这些反过来被确认为握手过程的一部分。下图显示了这种三向握手的建立。
- 发起方(客户端)发送一个段,其中设置了 SYN 标志,并在序列号字段 (SEQ = X) 中设置初始序列号。
- 响应方(服务器端)收到此消息后,记录传入方向的序列号,然后返回一个段,其中设置了 SYN 和 ACK 标志,其中序列号字段,设置为其自己为反向分配的值 (SEQ = Y),以及 X + 1 (PACK = X + 1) 的捎带式确认字段,以确认它已注意到其传入方向的初始值。
- 发起方收到此消息后,返回一个段,其中设置了ACK 标志和序列号Y + 1。
三路握手建立链接
TCP用于建立和终止连接的算法称为三路握手。我们首先描述基本算法,然后展示它是如何被 TCP 使用的。三路握手涉及在客户端和服务器之间交换三个消息,见下图。
这个想法是,双方想要就一组参数达成一致,在打开TCP连接的情况下,这些参数是双方计划用于各自字节流的起始序列号。通常,参数可以是每一方希望另一方知道的任何事实。
- 首先,客户端(主动参与者)向服务器(被动参与者)发送一个段,说明它计划使用的初始序列号(Flags = SYN,SequenceNum = x)。
- 服务器用一个片段进行响应,该片段既确认客户端的序列号(Flags = ACK,Ack = x + 1)并声明它自己的起始序列号(Flags = SYN,SequenceNum = y)。也就是说,SYN 和 ACK 位都设置在该第二个消息的标志字段中。
- 客户端用第三段响应服务器的序列号(Flags = ACK,Ack = y + 1)。
每一方确认的序列号比发送的序列号大一,其原因是,确认字段实际上标识了“预期的下一个序列号”,从而隐含地确认所有较早的序列号。尽管未在此时间线中显示,但会为前两个段中的每一个安排一个计时器,如果未收到预期响应,则重新传输该段。
为什么客户端和服务器必须在连接建立时,相互交换起始序列号。如果每一端只是从某个“众所周知”的序列号开始,比如 0,那会更简单。事实上,TCP规范要求连接的每一端随机选择一个初始的起始序列号。这样做的原因是,为了防止同一连接的两个化身过早地重用相同的序列号。
三路握手打开和关闭连接
下图显示了一个基本的三向握手。步骤是:
- 发起者的初始状态是 CLOSED,而接收者的初始状态是 LISTEN。
- 发起方进入 SYN-SENT 状态,发送一个设置了 SYN 位的数据包,其起始序列号为 999(当前序列号,因此下一个发送的数字为 1000)。当接收方收到此消息时,进入 SYN-RECEIVED 状态。
- 接收者发回一个设置了 SYN 和 ACK 位的TCP数据包,这表明它是一个 SYN 数据包,并且它正在确认前一个 SYN 数据包。在这种情况下,接收者告诉发起者它将从 100 的序列号开始传输。确认号是 1000,这是接收者希望接下来接收的序列号。发起方收到此消息后,进入 ESTABLISHED 状态。
- 发起方发回一个TCP数据包,其中设置了ACK 位,确认号为 101,这是它希望接下来看到的序列号。
- 始发者发送序列号为 1000 的数据。
三路握手防止重复连接
下图显示了三路握手如何防止旧的重复连接启动造成混淆。在状态 3 中,收到了来自先前连接的重复 SYN。接收者发回对此的确认 (4),但是,当发起者收到此确认时,发起者发回 RST重置数据包。这会导致接收者返回到 LISTEN 状态。然后,它会收到2中发送的SYN数据包,并在确认之后,建立连接。
如果其中一个 TCP 已关闭或中止,而另一端仍处于连接状态,则 TCP 连接将处于半开状态。如果两个连接由于系统崩溃,而变得不同步,半开状态也可能发生。如果数据以任一方向发送,此连接将自动重置。这是因为序列号不正确,否则连接将超时。
三路握手关闭连接
通常使用 CLOSE 调用关闭连接。关闭的主机不能继续发送,但可以继续接收,直到对方告诉它关闭。下图显示了关闭连接的典型顺序。通常,应用程序为给定的连接发送一个 CLOSE 调用。接下来,发送一个设置了 FIN 位的 TCP 数据包,发起方进入 FIN-WAIT-1 状态。当另一个 TCP 已经确认了FIN, 并发送了自己的 FIN 时,第一个TCP可以确认这个 FIN。
应用程序设计
服务器端
套接字(socket)创建:
int sockfd = socket(domain, type, protocol)
sockfd:套接字描述符,一个整数
domain:整数,通信域,例如 AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)
type:通讯型
- SOCK_STREAM:TCP(可靠,面向连接)
- SOCK_DGRAM:UDP(不可靠,无连接)
protocol:Internet 协议 (IP) 的协议值,为 0。这与出现在数据包 IP 标头中的协议字段上的数字相同。
设置选项:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
这有助于操作由文件描述符 sockfd 引用的套接字的选项。这是完全可选的,但它有助于重用地址和端口。防止诸如“地址已在使用中”之类的错误。
绑定:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
创建套接字后,bind 函数将套接字绑定到 addr(自定义数据结构)中指定的地址和端口号。在示例代码中,我们将服务器绑定到本地主机,因此我们使用 INADDR_ANY 来指定 IP 地址。
监听:
int listen(int sockfd,int backlog);
它将服务器套接字置于被动模式,等待客户端接近服务器以建立连接。积压,定义了 sockfd 的挂起连接队列可以增长到的最大长度。如果连接请求在队列已满时到达,客户端可能会收到带有 ECONNREFUSED 指示的错误。
接受客户端的连接请求:
int new_socket= accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
它提取侦听套接字 sockfd 的挂起连接队列中的第一个连接请求,创建新的连接套接字,并返回引用该套接字的新文件描述符。此时,客户端和服务器之间建立连接,准备传输数据。
客户端
套接字创建:
与服务器端创建套接字完全相同
连接:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect() 系统调用将文件描述符 sockfd 引用的套接字连接到 addr 指定的地址。服务器的地址和端口在 addr 中指定。
setsockopt与套接字行为控制
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
该函数应在 level 参数指定的协议级别,将 option_name 参数指定的选项设置为 option_value 参数所指向的值。
level 参数指定选项所在的协议级别。要在套接字级别设置选项,level参数为 SOL_SOCKET。如果·要在其他级别设置选项,控制选项的协议需要提供适当的级别标识符。例如,要指示选项由 TCP(传输控制协议)解释,则level应设置为IPPROTO_TCP。
option_name 参数指定要设置的单个选项。 option_name 参数和任何指定的选项都直接传递给适当的协议模块,以进行解释。 <sys/socket.h> 头文件定义了套接字级别的选项。选项如下:
- SO_DEBUG
打开调试信息的记录。此选项启用或禁用底层协议模块中的调试。此选项采用 int 值。这是一个布尔选项。 - SO_BROADCAST
如果协议支持,则允许发送广播消息。此选项采用 int 值。这是一个布尔选项。 - SO_REUSEADDR
指定用于验证提供给 bind() 的地址的规则,应该允许重用本地地址,如果协议支持的话。此选项采用 int 值。这是一个布尔选项。 - SO_KEEPALIVE
如果协议支持,则通过启用消息的定期传输来保持连接处于活动状态。此选项采用 int 值。
如果连接的套接字未能响应这些消息,则连接中断,写入该套接字的线程会收到 SIGPIPE 信号通知。这是一个布尔选项。
- SO_LINGER
如果数据存在,则停留在 close() 上。此选项控制在套接字上的未发送消息排队,并执行 close() 时采取的操作。如果设置了 SO_LINGER,系统将在 close() 期间阻塞进程,直到它可以传输数据或直到时间到期。如果未指定 SO_LINGER,并发出 close(),则系统以允许进程尽快继续的方式处理调用。此选项采用 <sys/socket.h> 标头中定义的延迟结构,以指定选项的状态和延迟间隔。 - SO_OOBINLINE
叶子收到内联的带外数据(标记为紧急的数据)。此选项采用 int 值。这是一个布尔选项。 - SO_SNDBUF
设置发送缓冲区大小。此选项采用 int 值。 - SO_RCVBUF
设置接收缓冲区大小。此选项采用 int 值。 - SO_DONTROUTE
请求传出消息绕过标准路由设施。目的地应位于直接连接的网络上,消息根据目的地地址定向到适当的网络接口。此选项的效果(如果有)取决于正在使用的协议。此选项采用 int 值。这是一个布尔选项。 - SO_RCVLOWAT
设置要为套接字输入操作处理的最小字节数。 SO_RCVLOWAT 的默认值为 1。如果 SO_RCVLOWAT 设置为较大的值,阻塞接收调用通常会等待,直到它们收到低水位标记值或请求数量中的较小者。 (如果发生错误、捕获信号或接收队列中的下一个数据类型与返回的数据类型不同,它们可能返回小于低水位线;例如,带外数据。)此选项采用一个整数值。 - SO_RCVTIMEO
设置超时值,该值指定输入函数在完成之前等待的最长时间。它接受带有秒数和微秒数的 timeval 结构,指定等待输入操作完成的时间限制。如果接收操作阻塞了这么长时间而没有接收到额外的数据,如果没有接收到数据,它将返回部分计数或设置为 [EAGAIN] 或 [EWOULDBLOCK] 的 errno。此选项的默认值为零,表示接收操作不会超时。此选项采用 timeval 结构。 - SO_SNDLOWAT
设置要为套接字输出操作处理的最小字节数。如果流量控制不允许,处理发送低水位标记值或整个请求中的较小者,则非阻塞输出操作不应处理任何数据。此选项采用 int 值。请注意,并非所有实现都允许设置此选项。 - SO_SNDTIMEO
设置超时值,指定输出功能阻塞的时间量,因为流控制阻止发送数据。如果发送操作在这段时间内被阻塞,如果没有数据发送,它将返回部分计数或将 errno 设置为 [EAGAIN] 或 [EWOULDBLOCK]。此选项的默认值为零,表示发送操作不会超时。
TCP层套接字行为控制代码
。
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
using namespace std;
int main()
{
int s;
int optval;
socklen_t optlen = sizeof(optval);
/* Create the socket */
if((s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror("socket()");
return -1;
}
/* Check the status for the keepalive option */
if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
perror("getsockopt()");
close(s);
return -1;
}
cout << "SO_KEEPALIVE is " << (optval ? "ON" : "OFF") << endl;
/* Set the option active */
optval = 1;
optlen = sizeof(optval);
if(setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, optlen) < 0) {
perror("setsockopt()");
close(s);
return -1;
}
cout << "SO_KEEPALIVE set on socket" << endl;
/* Check the status again */
if(getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen) < 0) {
perror("getsockopt()");
close(s);
return -1;
}
cout << "SO_KEEPALIVE is " << (optval ? "ON" : "OFF") << endl;
close(s);
return 0;
}
输出结果
SO_KEEPALIVE is OFF
SO_KEEPALIVE set on socket
SO_KEEPALIVE is ON
客户/服务器(Client/Server)设计模型
TCP客户/服务器
服务器
服务器端代码
。
#include <iostream>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <thread>
#include <memory>
#include <map>
#include <mutex>
#define MAX 80
#define PORT 8080
#define SA struct sockaddr
using namespace std;
class chatgroup {
public:
static shared_ptr<chatgroup> m_pChatGroup;
static shared_ptr<chatgroup> getInstance()
{
if (m_pChatGroup == nullptr) {
m_pChatGroup = make_shared<chatgroup>();
}
return m_pChatGroup;
}
void add_new_member(string name, int socket_fd)
{
lock_guard<mutex> lk(lock);
members[name]=socket_fd;
}
void add_new_member(int snd_fd, int socket_fd)
{
lock_guard<mutex> lk(lock);
cout << "snd:" << snd_fd << " rcv:"<<socket_fd<<endl;
pair_members[snd_fd]=socket_fd;
}
int find_member(string name)
{
lock_guard<mutex> lk(lock);
map<string, int>::iterator it;
it = members.find(name);
if (it == members.end())
return -1;
return it->second;
}
int find_member(int snd_fd)
{
lock_guard<mutex> lk(lock);
map<int, int>::iterator it;
it = pair_members.find(snd_fd);
if (it == pair_members.end())
return -1;
return it->second;
}
private:
map<string, int> members;
map<int, int> pair_members;
mutex lock;
};
shared_ptr<chatgroup> chatgroup::m_pChatGroup=nullptr;
// Function designed for chat between client and server.
void task_server(int sockfd)
{
char buff[MAX];
int n;
shared_ptr<chatgroup> m_pInst = chatgroup::getInstance();
for (;;) {
bzero(buff, MAX);
read(sockfd, buff, sizeof(buff));
cout << "From client: " << buff << endl;
buff[strlen(buff)-1] = '\0';
string s(buff);
size_t found = s.find("login:", 0);
if (found!=string::npos) {
string name=s.substr(found+6);
cout << "name: " << name << endl;
m_pInst->add_new_member(name, sockfd);
strcat(buff, ", this is server reply\n");
write(sockfd, buff, sizeof(buff));
continue;
}
found = s.find("sendto:", 0);
if (found!=string::npos) {
string name=s.substr(found+7);
cout << "name: " << name << endl;
int fd = m_pInst->find_member(name);
if (sockfd == fd)
{
strcpy(buff, "you can't talk to yourself\n");
}
else if (fd == -1) {
sprintf(buff, "%s doesn't exist\n", name.c_str());
}
else
{
strcpy(buff, "done\n");
m_pInst->add_new_member(sockfd, fd);
}
write(sockfd, buff, sizeof(buff));
continue;
}
found = s.find("msg:", 0);
if (found!=string::npos) {
string msg=s.substr(found+4);
cout << "msg: " << msg << endl;
int fd = m_pInst->find_member(sockfd);
write(fd, msg.c_str(), msg.length());
cout << "send msg to: " << fd << endl;
strcpy(buff, "done\n");
write(sockfd, buff, strlen(buff));
continue;
}
strcat(buff, "whom you want to talk?\n");
n = 0;
write(sockfd, buff, sizeof(buff));
if (strncmp("exit", buff, 4) == 0) {
cout << "Server Exit..." << endl;
break;
}
}
}
int main()
{
int sockfd, connfd;
struct sockaddr_in servaddr, cli;
socklen_t len;
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
cout << "socket creation failed..." << endl;
exit(0);
}
cout << "Socket successfully created.." << endl;
bzero(&servaddr, sizeof(servaddr));
// assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
// Binding newly created socket to given IP and verification
if ((bind(sockfd, (SA*)&servaddr, sizeof(servaddr))) != 0) {
cout << "socket bind failed..." << endl;
exit(0);
}
cout << "Socket successfully binded.." << endl;
// Now server is ready to listen and verification
while (1)
{
if ((listen(sockfd, 5)) != 0) {
printf("Listen failed...\n");
break;
}
cout << "Server listening.." << endl;
len = sizeof(cli);
// Accept the data packet from client and verification
connfd = accept(sockfd, (SA*)&cli, &len);
if (connfd < 0) {
printf("server acccept failed...\n");
continue;
}
thread(task_server, connfd).detach();
}
close(sockfd);
}
客户
客户端代码
。
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define MAX 80
#define PORT 8080
#define SA struct sockaddr
using namespace std;
void task_client(int sockfd, char *name)
{
char buff[MAX];
int n;
sprintf(buff, "login:%s\n", name);
write(sockfd, buff, sizeof(buff));
bzero(buff, sizeof(buff));
read(sockfd, buff, sizeof(buff));
printf("From Server : %s", buff);
for (;;) {
bzero(buff, sizeof(buff));
printf("Enter the string : ");
n = 0;
while ((buff[n++] = getchar()) != '\n')
;
write(sockfd, buff, sizeof(buff));
bzero(buff, sizeof(buff));
read(sockfd, buff, sizeof(buff));
printf("From Server : %s", buff);
if ((strncmp(buff, "exit", 4)) == 0) {
printf("Client Exit...\n");
break;
}
}
}
int main(int argc, char *argv[])
{
int sockfd, connfd;
struct sockaddr_in servaddr, cli;
if (argc != 2)
{
cout << "Usage:" << endl;
cout << " tcp_client.bin port name" << endl;
exit(0);
}
// socket create and varification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
cout << "socket creation failed..." << endl;
exit(0);
}
cout << "Socket successfully created.." << endl;
bzero(&servaddr, sizeof(servaddr));
// assign IP, PORT
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(PORT);
// connect the client socket to server socket
if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) != 0) {
printf("connection with the server failed...\n");
exit(0);
}
cout << "connected to the server.." << endl;
// function for chat
task_client(sockfd, argv[1]);
// close the socket
close(sockfd);
}