网络编程
TCP传输控制协议
传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol)
一、TCP协议的特点:TCP是面向连接的,端对端(一对一)的可靠的协议,可修复损坏数据,全双工的连接。
1.TCP协议判断数据是否损坏的方式是给每个数据段都添加校验和,接收端收到数据段进行校验,如果校验失败则丢弃已经损坏的数据段,也并不会进行确认应答,所以TCP协议会再次传输数据段,且修复损坏、出现丢失、出现重复、到达无序的数据。
2.另外,接收端每次接收到数据之后必须发送确认应答(ACK),如果在超时时间内没有发送应答信号(ACK),则TCP协议会认为数据没有送达,则会重新发送数据。
3.因为通信可能是建立在不可靠的网络中以及网络层不可靠的传输机制,所以TCP协议中采用了一种*基于时钟的序列号握手机制*实现双方的有效连接。
二、TCP报首的格式
源端口号:发送端的进程端口号,两个字节。
目标端口:接收端的进程端口号,两个字节。
序列号SEQ:序列号主要是为了解决网络包乱序的问题,指的是数据段中的第一个字节的序列号,如果数据段中存在SYN标志位,则序列号为初始序列号(ISN),那么数据段中的第一个字节的序列号等于 ISN + 1。
控制位:SYN同步请求连接;确认应答号ACK;结束标志FIN。
头部长度:用于表示数据的开始位置,至少是20字节。
校验和:用于解决数据出现丢包、重复、无序、损坏的问题,接收端收到数据段会进行校验,当校验失败时会丢弃损坏的数据段。
窗口号:用于表示接收端剩余空间的值。
三、TCP的通信流程
采用C/S模式可以把双方分为主动连接端/客户端(client)和被动连接端/服务端(server)。
1) socket()函数:int socket(int domain, int type, int protocol);
1.参数 int domain(协议族), int type(socket的类型), int protocol(协议)
2.返回值 套接字的文件描述符。
3.例子 socket(AF_INET,SOCK_STREAM,0);
客户端(client)
*2) connect()函数:int connect(int sockfd, const struct sockaddr addr, socklen_t addrlen);
1.参数 int sockfd(文件描述符), const struct sockaddr *addr(目标主机的地址), socklen_t addrlen(目 标主机的地址结构的大小,可用sizeof计算)
2.返回值 成功则返回0,失败则返回-1,一般情况下只能成功连接一次。
3.注意 如果调用connect()函数连接目标主机失败,则调用方的套接字状态是未指定的,所以应该关闭 套接字,并重新创建一个新的套接字,然后再次连接即可。
**2) send()函数:ssize_t send(int sockfd, const void *buf, size_t len, int flags); **
1.参数 int sockfd(文件描述符),const void *buf(待发送数据的缓冲区), size_t len(待发送数据的长度), int flags(发送标志)
2.返回值 成功返回实际发送的字节个数,失败返回-1并且会返回错误码。
3.注意 ①send()函数只能用于套接字处于连接的状态。
②另外,如果要发送的数据的不适合套接字的发送缓冲区,则send()函数通常会阻塞,除非套 接字设置为非阻塞模式(如果套接字处于非阻塞模式,则send()函数会调用失败)。
③发送端发送的数据会暂存在接收端的内核缓冲区中,如果接收端一直不接收数据,则会导致 接收缓冲区满,这样发送端调用send()函数继续发送数据时也会阻塞。
服务端(server)
1) listen()函数:int listen(int sockfd, int backlog);
1.参数 int sockfd(被设置为监听状态的套接字),int backlog(等待接受连接的客户端的最大长度)。
2.返回值 成功则返回0,失败则返回-1。
3.注意 listen()函数是不会阻塞的。
**2) accept()函数:int accept(int sockfd, struct sockaddr addr, socklen_t addrlen);
1.参数 int sockfd(被设置为监听状态的套接字), struct sockaddr *addr(对方主机地址的结构指针), socklen_t *addrlen(对方主机的地址信息长度)
2.返回值 成功会返回一个新的套接字文件描述符,失败则返回-1。
3.注意 accept()函数会从等待连接的队列中提取第一个客户端进行连接,如果此时等待队列中没有等 待连接的客户端,则accept()函数会阻塞,直到等待队列中存在客户端请求。
*3) recv()函数:ssize_t recv(int sockfd, void buf, size_t len, int flags);
1.参数 int sockfd(accept()函数的返回的套接字), void *buf(接收到的数据要存储的缓冲 区), size_t len(要接收的数据的大小), int flags(接收标志)
2.返回值 成功返回接收到的字节个数,失败则返回-1,如果客户端已经关闭连接,则返回值为0,如果 没有收到数据,则recv()函数会默认阻塞,直到有数据到达。
四、TCP的握手机制
TCP的三次握手是在客户端调用connect()期间和服务器调用accept()期间才会发生的。
五、TCP的数据缓冲区
1.对于TCP通信而言,是具有发送缓冲区和接收缓冲区的,发送端具有发送缓冲区,接收端具有接收缓冲区。想要设置接收缓冲区的大小,则需要通过设置套接字的属性选项实现,Linux系统中提供了两个函数接口来获取和设置套接字的属性选项,分别是getsockopt()和setsockopt()。
**2)getsockopt()函数:int getsockopt(int sockfd,int level,int optname,void optval,socklent optlen);
*3)setsockopt()函数:int setsockopt(int sockfd, int level, int optname, const void optval, socklen_t optlen);
1.参数 int sockfd(套接字对应的文件描述符)
int level(选项对应的协议级别,一般为SOL_SOCKET)
int optname(属性名称,例如为接收缓冲区则填SO_RCVBUF,详细如下图)
void *optval(设置的选项值)
socklent *optlen(选项值的长度,数据类型的大小)
2.返回值 成功返回0,失败返回-1并且会返回错误码。
3.注意 如果打算设置TCP接收缓冲区大小,应该在调用listen()函数之前进行设置才会生效!!!
4)设置TCP的接收缓冲区为某个固定值
可以把TCP接收缓冲区理解为一个水池,在水到达水位线时才能输出。在Linux系统中缓冲区初始化为1,且只有接收缓冲区允许被修改。
六、TCP的OOB(Out of Band)带外数据
通过特殊的标志位,让数据可以不受缓冲区和水位线的限制,可以优先读取,但是带外数据每次只能发送一个字节,可以发送多次,由于recv()函数和send()函数的第4个参数才可以指定MSG_OOB标志,所以紧急带外数据只能通过recv()和send()这两个函数进行收发。带外数据也是通过内核缓冲区发送出去,所以接收端接收到之后也需要从内核缓冲区中读取出来。并且接收端需要对SIGURG信号进行注册,但是由于信号触发之后会中断程序执行,应该确保信号响应接口只是完成接收动作,应该确保响应函数立即结束。
七、套接字的超时控制
1)服务器接受客户端的连接请求成功建立连接外还存在两种情况会导致程序逻辑异常:
1.服务器默认设置是阻塞模式,服务器运行之后如果一直没有客户端发起连接请求,此时服务器会阻塞在accept()函数的位置。
2.把服务器的套接字设置为非阻塞模式,如果服务器没有监听到有客户端发起连接请求,则服务器的accept()函数会立刻返回。
2)解决方案
1.避免长时间阻塞应使用setsockopt设置套接字为非阻塞模式并且设置超时机制,如果超时时间设置为0,则操作永远不会超时。
2.如果要设置超时时间,需要利用名称叫做struct timeval的结构体,结构体有2个成员,成员tv_sec指的是秒数,成员tv_usec指的是微秒数。