TCP允许程序员在两个应用程序之间建立连接并来回传递数据,这种应用程序可以在同一台机器上,也可以在不同的机器上运行。TCP并没有规定应用程序在什么时间进行交互以及为什么要进行交互,也没有规定程序员如何组织这些应用程序。在实践中,有一种应用程序的组织方式在TCP的使用中占据着主要地位,就是C/S模式,即Client/Server模式。
C/S模式要求应用程序分为两个部分:一个负责通过发送请求来启动通信,称为Client,另一个负责对请求进行应答,称为Server。Client和Server通常运行于不同的机器上。
Server端代码示例如下:
fd = socket(AF_INET, SOCK_STREAM, 0);
...
local_addr.sin_family = AF_INET;//使用IPv4协议
local_addr.sin_port = htons(80);//监听80端口
local_addr.sin_addr.s_addr = 192.168.1.101;//绑定IP地址,INADDR_ANY是任意地址
bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr));
...
listen(fd, 10);
...
while(1) {
sockfd = accept(fd, (struct sockaddr *)&addr, &addrlen);
...
while(1) {
rlen = read(sockfd, buf, len);
if (rlen == 0) {
break;
}
...
wlen = write(sockfd, buf, len);
...
}
close(sockfd);
...
}
...
close(fd);
...
Server端代码详解:
使用socket系统调用可以获得一个socket的文件描述符,利用这个描述符就可以控制socket的行为。示例中的调用方法可以产生一个使用TCP协议的socket。
bind系统调用用于将socket绑定到指定的IP和端口上,这样Server端的TCP就会使用这个指定的IP-端口对来提供服务。这样的IP-端口对必须对外发布,使得想要与server进行通信的client端知道如何找到server并与之建立连接。同样的IP-端口对只能bind一次。
listen系统调用设置socket为监听状态,并设置等待建立的连接的队列长度为10。
accept系统调用在没有SYN请求到来时会阻塞,有SYN请求到来并且三次握手完成后会解除阻塞,并产生一个新的socket sockfd,与client端进行数据收发的通信由sockfd完成。同时有多个请求到来并且完成三次握手时,未被accept的连接会放入队列中等待accept,这个队列的长度由listen系统调用决定。
read系统调用将client发送的数据读取出来,读出的数据长度为0意味着client端已经关闭了连接。
write系统调用将server端的数据发送给client端。
close系统调用会释放socket,在释放前TCP会关闭与client之间的连接。
完成与client端的一次通信后,server会重新调用accept,等待新连接到来。
client端代码示例:
fd = socket(AF_INET, SOCK_STREAM, 0);
daddr.sin_family = AF_INET;
daddr.sin_port = htons(80);
daddr.sin_addr.s_addr = 192.168.1.101;//Server IP地址
connect(fd, (struct sockaddr *)&daddr, sizeof(daddr));
...
while(1) {
wlen = write(sockfd, buf, len);
...
rlen = read(sockfd, buf, len);
if (rlen == 0) {
break;
}
...
}
close(fd);
client端代码详解:
socket系统调用产生一个client端的TCP socket,在整个通信过程中client只使用这一个socket。
connect系统调用用于向server发起连接请求,对于TCP则会发送SYN请求,开始三次握手过程。这个过程完毕后connect会返回。
其余的系统调用的功能与server端一致。
client也可以使用bind系统调用,不过client使用bind是绑定通信时使用的源端口号。如果不调用bind则在调用connect时TCP会自动选取一个端口号作为源端口。
使用上述模型就可以利用TCP完成数据通信了。对代码的解说也能使得socket API接口对TCP行为的影响明晰一些。但还有很多问题无法明确,例如:socket系统调用究竟对TCP做了什么?TCP如何选取client的源端口号?对于这样的问题,让我们到Linux内核代码中去寻找答案吧!