其实在写这篇文章开始之前,原本想打算先介绍一下TCP/IP协议的内容,但是在网上看了一些博客,大概都讲的差不多,随便找几篇博客来看(https://developer.51cto.com/art/201906/597961.htm),你就会对这个协议有一个大概的了解(有些地方或许读者和我一样可能也看的不是很明白,但是这对编程阻碍不大),所以我也不打算写这个了(理由是,自己也比较菜,只要大概了解一下这部分内容就行,在日后学习或者工作当中遇到什么不理解的地方再去深入学,比较有针对性;所以侧重点还是在编程上,最终实现理论转到实践当中去,才是王道)。不过经典的TCP三次握手和四次挥手告别,这个基本你必须要明白,这里简单介绍一下,那么就开始今天的内容了。
一、TCP三次握手和四次挥手告别:
(1)三次握手过程(要明白一点TCP传输数据是可靠的,所以才要这套机制):
- 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发(这个下面接口函数会有介绍的)。
- 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
- 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J 1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
- 第三次握手:客户端收到确认后,检查ack是否为J 1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K 1,并将该数据包发送给服务器端,服务器端检查ack是否为K 1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
(2)四次挥手告别:
- 四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
- 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
a、第一次次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
b 、第二次挥手:服务器端收到FIN后,先发送ack=M 1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
c、第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
d、第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N 1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次挥手告别。
(3)注意:上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况。
二、什么是socket?:
在讲解这个之前,你得必须要明白在Linux系统下,一切皆是文件,(举个简单的例子,一般我们在Windows系统下,你是可以清楚的看到硬盘盘符,并标有大小的,可以直接用鼠标拖拉文件到里面去;而在Linux系统下是看不到的,它是以文件的形式存在的,对它操作可就没那么简单了)。为啥这里要说一下这个呢?因为后面接口函数里面的参数就有这个socket描述符(如果你第一次看到这个可能就有点懵逼了,不知道为啥要有这个东西),其实它和我们之前一系列的文章里讲的文件描述符是一样的(文件描述符这里就不介绍了,前面对文件操作的文章里面已经很详细的介绍了这个,读者可以去看我之前写的文章,如有不理解的地方,欢迎来交流)
其实这个socket就是套接字(套接字是网络数据传输用的软件设备;这里有一个比较形象的比喻,因为socket这个英文单词的中文意思是插座的意思,所以我们把插头插到插座上就能从电网获得电力供给),同样,为了与远程计算机进行传输数据,需要连接到因特网,而编程套接字就是用来连接该网络的工具。
三、使用打电话的形式来介绍socket接口函数:
1、调用socket函数(安装电话机)时进行的通话:
问:"接电话需要准备什么?"
答:"当然是电话机!"
说明:
int domain(这里domain的中文意思是领域,域名):它是创建套接字所使用的协议栈,通常为AF_INET(也就是IPv4网络协议),下面试各种协议栈的汇总:
代码语言:javascript复制DESCRIPTION
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a
successful call will be the lowest-numbered file descriptor not currently open for the process.
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families
are defined in <sys/socket.h>. The currently understood formats include:
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk ddp(7)
AF_PACKET Low level packet interface packet(7)
AF_ALG Interface to kernel crypto API
type:它是用来指定套接字的类型,即指定数据流套接字(SOCK_STREAM)还是数据报套接字(SOCK_DGRAM):
代码语言:javascript复制 The socket has the indicated type, which specifies the communication semantics. Currently defined types are:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be sup‐
ported.
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a con‐
sumer is required to read an entire packet with each input system call.
SOCK_RAW Provides raw network protocol access.
SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET Obsolete and should not be used in new programs; see packet(7).
Some socket types may not be implemented by all protocol families.
Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it may include the bitwise OR of any
of the following values, to modify the behavior of socket():
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to
achieve the same result.
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for
reasons why this may be useful.
protocol:它通常设置为0,函数返回一个socket描述符(上面已有提醒这个哦)。
2、调用bind函数(分配电话号码)时进行的对话:
- 问:“请问您的电话号码是多少?”
- 答:“我的电话号码是192168。”
套接字也是这样。就像电话机分配电话号码一样(虽然不是真的把电话号码分配给电话机),这里类似的用bind函数给创建好的套接字分配地址信息(地址信息包括:IP地址和端口号,这个如果不知道的话,也可以去查看百度或者博客,这里就不介绍了。)。
下面我们来看一下在Linux系统下它的原型:
代码语言:javascript复制BIND(2)
Linux Programmer's Manual
BIND(2)
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
说明:
int sockfd:这个就是socket文件描述符了。
const struct sockaddr *addr:这个参数可以看它的源注解,下它里面讲解的那个地址查询,还是用man手册来查看(例如man ipv6):
代码语言:javascript复制 When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the
address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size, in bytes, of the address
structure pointed to by addr. Traditionally, this operation is called “assigning a name to a socket”.
It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may receive connections (see accept(2)).
The rules used in name binding vary between address families. Consult the manual entries in Section 7 for detailed information. For AF_INET,
see ip(7); for AF_INET6, see ipv6(7); for AF_UNIX, see unix(7); for AF_APPLETALK, see ddp(7); for AF_PACKET, see packet(7); for AF_X25, see
x25(7); and for AF_NETLINK, see netlink(7).
The actual structure passed for the addr argument will depend on the address family. The sockaddr structure is defined as something like:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings. See EXAMPLE below.
socklen_t addrlen:这个参数就是指分配地址空间的大小
3、调用listen函数(连接电话)时进行的对话:
- 问:“已经架好电话机后是否只需连接电话线?”
- 答:“对,只需连接电话线就能打电话通信了!”
一连接电话线,电话机可以转为可接听的状态,这时其他人可以拨打电话请求连接到该机。同样需要把套接字转化成可接收连接的状态。下面是他函数原型:
代码语言:javascript复制 LISTEN(2)
Linux Programmer's Manual
LISTEN(2)
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
说明:
这里主要讲第二参数 int backlog,它 是用来设置请求队列中允许的最大请求数。注意:TCP传输中为被动套接字设置了两个队列:完全建立连接的队列和未完全建立连接的队列。
代码语言:javascript复制 The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the
client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt
at connection succeeds.
4、调用accept函数(拿起话筒)时进行的对话:
- 问:"电话铃声响了,我该怎么办?"
- 答:"难道您真不知道?马上接啊!"
拿起话筒意味着接收了对方的连接请求。套接字也是这样,如果有人为了完成数据传输而请求连接,就需要调用accept函数来进行处理。我们来看一下它的原型:
代码语言:javascript复制ACCEPT(2)
Linux Programmer's Manual
ACCEPT(2)
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
说明:
这里主要说一下第四个参数int flags(这里我没有翻译,看原注释更好一点,自己翻译翻着意思就变了。哈哈,还是自己的英文水平比较菜):
代码语言:javascript复制 If flags is 0, then accept4() is the same as accept(). The following values can be bitwise ORed in flags to obtain different behavior:
SOCK_NONBLOCK Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve the same result.
SOCK_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be use‐
ful.
四、总结:
socket编程中接收连接请求的套接字创建过程如下:
- 第一步:调用socket函数来创建套接字。
- 第二步:调用bind函数分配IP地址和端口号。
- 第三步:调用listen函数转为可接收状态。
- 第四步:调用accept函数来处理连接请求。
- 第五步:关闭套接字文件描述符。