目录
基础篇
一.TCP概述
1.什么是TCP连接
首先,每一条TCP连接有两个端点,而TCP连接的端点叫做套接字( socket)。根据定义:端口号拼接到 IP地址即构成了套接字。因此,套接字的表示方法是在点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。例如,若IP地址是192.3.4.5而端口号是80,那么得到的套接字就是
(192.3.4.5:80).总之,我们有,每一条TCP连接唯一地被通信两端的两个端点(即两个套接字)所确定。即:
一定要记住:TCP连接的端点是个很抽象的套接字,即(IP地址:端口号)。也还应记住:同一个IP地址可以有多个不同的TCP连接,而同一个端口号也可以出现在多个不同的TCP连接中。
2.TCP的特点
- TCP是面向连接的运输层协议。这就是说,应用程序在使用TCP协议之前,必须先建立TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接。也就是说,应用进程之间的通信好像在“打电话”:通话前要先拨号建立连接,通话结束后要挂机释放连接。
- 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(一对一)。
- TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复,并且按序到达。
- TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。在发送时,应用程序在把数据传送给TCP的缓存后,就可以做自己的事,而TCP在合适的时候把数据发送出去。在接收时,TCP把收到的数据放入缓存,上层的应用进程在合适的时候读取缓存中的数据。
- 面向字节流。TCP中的“流”指的是流入到进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。TCP并不知道所传送的字节流的含义。TCP不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系。但接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。
3.TCP可靠传输的工作原理
下面为了讨论问题的方便,我们仅考虑A发送数据而B接收数据并发送确认。因此A叫做发送方,而B叫做接收方。
(1)无差错情况
停止等待协议可用下图来说明。图(a)是最简单的无差错情况。A发送分组M,发完就暂停发送,等待B的确认。B收到了M1就向A发送确认。A在收到了对M1的确认后,就再发送下一个分组M2.同样,在收到B对M2的确认后,再发送M3。
(2)出现差错
下图(b)是分组在传输过程中出现差错的情况。B接收M1时检测出了差错,就丢弃M1,其他什么也不做(不通知A收到有差错的分组)。也可能是M1在传输过程中丢失了,这时B当然什么都不知道。在这两种情况下,B都不会发送任何信息。可靠传输协议是这样设计的:A只要超过了一段时间仍然没有收到确认,就认为刚才发送的分组丢失了因而重传前面发送过的分组。这就叫做超时重传。要实现超时重传,就要在每发送完一个分组时设置一个超时计时器。如果在超时计时器到期之前收到了对方的确认,就撤销已设置的超时计时器。分组和确认分组都必须进行编号。这样才能明确是哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认。
(3)确认丢失和确认迟到
下图(a)说明的是另一种情况。B所发送的对M1的确认丢失了。A在设定的超时重传时间内没有收到确认,并无法知道是自己发送的分组出错、丢失,或者是B发送的确认丢失了。因此A在超时计时器到期后就要重传M1.现在应注意B的动作。假定B又收到了重传的分组M1。这时应采取两个行动
- 丢弃这个重复的分组M1,不向上层交付。
- 向A发送确认。
下图(b)也是一种可能出现的情况。传输过程中没有出现差错,但B对分组M1的确认迟到了。A会收到重复的确认。对重复的确认的处理很简单:收下后就丢弃。B仍然会收到重复的M1,并且同样要丢弃重复的M1,并重传确认分组。
二.TCP编程框架
服务器进程要先于客户进程启动:
- 调用socket()函数创建一 套接字,返回套接字s。
- 调用bind()函数将套接字s绑定到一个本地的端点地址上。
- 调用listen()函数将套接字s设置为监听模式,准备好接受来自各个客户的连接请求
- 调用accept()函数等待接受客户的连接请求。
- 如果接收到客户的连接请求,则accept()函数返回,得到新的套接字ns。
- 调用recv()函数在套接字ns上接收来自客户的数据。
- 处理客户的服务器请求。
- 调用send()函数在套接字ns上向客户发送数据。
- 与客户的通信结束后,服务器进程可以调用shutdown()函数通知对方不再发送或 据,也可以由客户进程断开连接。断开连接后,服务器进程调用closesocket()函数关闭套 ns。此后服务器进程继续等待客户进程的连接,回到第4步。
- 如果要退出服务器进程,则调用closesocket()函数关闭最初的套接字s。
客户进程的每一步骤如下:
- 调用socket()函数创建一 套接字,返回套接字s。
- 调用connect()函数将套接字s连接到服务器。
- 调用send()函数向服务器发送数据,调用recv()函数接收来自服务器的数据。
- 与服务器的通信结束后,客户进程可以调用shutdown()函数通知对方不再发送或 据,也可以由服务器进程断开连接。断开连接后,客户进程调用closesocket()函数关闭套接字s。
三.TCP程序设计常用函数
1、connect()函数
#include <sys/socket.h>
int connect( int sockfd,
const struct sockaddr *addr,
socklen_t len );
- 功能: 主动跟服务器建立连接,有点类似于,我们给别人电话,主动拨对方的电话号码。开启三次握手。
- 参数:
- sockfd:socket()返回的套接字,可以理解为客户端自己的套接字。
- addr:连接的服务器地址结构。
- len:地址结构体长度;
- 返回值: 成功:0 ,失败:-1。
2、send()函数
#include <sys/socket.h>
ssize_t send(int sockfd,
const void* buf,
size_t nbytes,
int flags);
- 功能: 发送数据,最后一个参数为 0 时,可以用 write() 替代( send 等同于 write )。注意:不能用 TCP 协议发送 0 长度的数据包。假如,数据没有发送成功,内核会自动重发。
- 参数:
- sockfd: 已建立连接的套接字,也可理解为发送端套接字描述符;
- buf: 指明一个存放应用程序要发送数据的缓冲区。
- nbytes: 指明实际要发送的数据的字节数。
- flags: 套接字标志,一般置0。
- 返回值: 成功:成功发送的字节数 ,失败:<0。
3.recv()函数
#include <sys/socket.h>
ssize_t recv(int sockfd,
void *buf,
size_t nbytes,
int flags);
- 功能: 接收网络数据,默认的情况下,如果没有接收到数据,这个函数会阻塞,直到有数据到来。
- 参数:
- sockfd: 已建立连接的套接字,也可理解为接收端套接字描述符。
- buf:指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据。
- nbytes:buf的长度。
- flags: 套接字标志,一般置0。
- 返回值: 成功:成功接收的字节数 ,失败:<0。
4.bind()函数
#include <sys/socket.h>
int bind( int sockfd,
const struct sockaddr *myaddr,
socklen_t addrlen );
- 功能: 将本地协议地址与 sockfd 绑定,这样socket的 ip、port 就固定了。bind只能绑定自身的地址及端口。在connect()或listen()调用前使用。
- 参数:
- sockfd: 表示已经建立的socket编号,也可理解为要进行绑定的套接字。
- myaddr:指向特定协议的地址结构指针。
- addrlen:该地址结构的长度;
- 返回值: 成功:返回 0, 失败:<0。
5.listen()函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- 功能: 将套接字由主动修改为被动,使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接。
- 参数:
- sockfd:用于标识一个已捆绑未连接套接口的描述字。
- backlog:等待连接队列的最大长度。
- 返回值: 成功:返回 0, 失败:其他。
6.accept()函数
#include <sys/socket.h>
int accept( int sockfd,
struct sockaddr *cliaddr,
socklen_t *addrlen );
- 功能: 从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)。
- 参数:
- sockfd: socket监听套接字。
- cliaddr: 用于存放客户端套接字地址结构。
- addrlen:套接字地址结构体长度的地址。
- 返回值: 成功:已连接的客户端的套接字, 失败:<0。
7.close()
#include <sys/socket.h>
int close( int sockfd)
- 功能:释放与sockfd相关联的TCP连接。
四.TCP程序设计代码(单客户端向服务端多次发送)
myclient.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);/*该函数返回一个类似于文件描述符的句柄,创建一个套接字,指向描述符表的入口
AF_INET表示IPV4协议
SOCK_STREAM表示TCP协议
0代表默认协议*/
assert( sockfd != -1 );
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//要访问服务端的端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//要访问的IP地址,相当于服务端的IP地址
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
printf("input:\n");//此时提示客户端输入数据
char buff[128]={
0
};
fgets(buff,128,stdin);//此时已输入
if(strncmp(buff,"end",3)==0)//如果此时输入的是"end"代表结束输入
{
break;
}
send(sockfd,buff,strlen(buff),0);//给客户端发送数据
memset(buff,0,128);
recv(sockfd,buff,127,0);//接受服务器返回的数据
printf("buff = %s\n",buff);
}
close(sockfd);
}
myserver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);/*该函数返回一个类似于文件描述符的句柄,创建一个套接字,指向描述符表的入口
AF_INET表示IPV4协议
SOCK_STREAM表示TCP协议
0代表默认协议*/
assert(sockfd != -1);
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;//协议簇
saddr.sin_port = htons(6000);//服务器自己的端口号
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//服务器自己的IP
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定监听的IP地址和端口到sockfd
assert(res != -1);
listen(sockfd,5);//5代表在请求队列中允许的最大请求数
//此时已完成三次握手
while(1)
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
//这里的caddr代表客户端的协议地址
//len代表客户端地址长度
if(c<0)
{
continue;
}
while(1)
{
printf("accept(ip:%s,pore:%d)c = %d\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),c);
char buff[128] = {0};
if(recv(c,buff,127,0) == 0)
{
break;
}
printf("buff = %s\n",buff);
send(c,"ok",2,0);
}
}
}
五.补充
1.流式套接字编程的适用场合
- 大数据量的数据传输应用。
- 可靠性要求高的传输应用。
2.服务器是如何处理多个客户服务请求的呢?
如果是循环服务器,则服务器在与一个客户建立连接后,其他客户只能等待; 客户服务完之后,服务器才会处理另一个客户的服务请求。
如果是并发服务器,则当服务器与一个客户进行通信的过程中,可以同时接收其他客户的服务请求,并且服务器要为每一个客户创建一个单独的子进程或线程,用新创建的套接字与每个客户进行独立连接上的数据交互。在并发服务器的通信流程中,通过第4步返回多个连接套接字,这些连接套接字在步骤5~9与多个客户通信时是并发执行的。
3.客户是否需要bind()操作?
服务器进程提供服务的端口通常是与这个熟知服务对应的知名端口,是设计人员为该服务 预留的端口,因此服务器进程通过bind()函数可以准确无误地将一个套接字与本地的主机地址和这个知名端口关联起来。 那么客户是否也要指定一个端点地址呢? 由于客户进程是请求服务的一方,设计人员没有必要为其指定固定的端口。如果客户进程 通过bind()函数强行将套接字与某一个端口绑定的话,有可能这个端口已经为其他套接字使用 了,则此时就会发生冲突,使通信无法正常进行。因此,对于客户进程,不鼓励将套接字绑定 到一个确定的端口上,实际上,在客户调用connect()或sendto()发送数据前,系统会从当前未使 用的端口号中随机选择一个,隐式调用一次bind()来实现客户套接字与本地地址的关联。 通过上述分析,我们明确在客户和服务器进行真正的数据通信前,双方的端点地址已经明 确,且与套接字关联在一起了。不过,关联操作可能是由用户通过显式的bind()函数调用指明 的,也可能是由系统在程序运行过程中选择并关联的。