一. Socket
Socket 工作位置
-
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
Socket 通信过程 - 先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束
二. 结构体
1.1 struct in_addr:32位IP地址
struct in_addr {
unsigned long s_addr; // 32位IP地址,4字节
};
1.2 struct sockaddr
struct sockaddr {
unsigned short sa_family; // 地址家族,通常为AF_INET,2字节
char sa_data[14]; // 目标地址和端口,14字节
};
1.3 struct sockaddr_in:
struct sockaddr_in {
short int sin_family; // 通信类型,通常为AF_INET,2字节
unsigned short int sin_port; // 端口,2字节
struct in_addr sin_addr; // Internet 地址,4字节
unsigned char sin_zero[8]; // 为保证与sockaddr结构的长度相同,8字节
};
三. 字节顺序转换
2.1 网络字节序与主机字节序
主机字节序就是常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指
整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的
定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0 ~ 7bit,其次8 ~ 15bit,
然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有
的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思
义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序
的问题了。
所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要
假定主机字节序跟网络字节序一样使用的是Big-Endian
-
大端序:整数高位字节在低地址内存,整数低位字节在高位地址
int a = 0x11223344 的大端序 -
小端序:整数高位字节在高地址内存,整数低位字节在低位地址
int a = 0x11223344 的小端序
2.2 h-host(本机),n-network(网络),s-short(短整型),l-long(长整型)
1)htons() -- "Host to Network Short"
2)htonl() -- "Host to Network Long"
3)ntohs() -- "Network to Host Short"
4)ntohl() -- "Network to Host Long"
5)inet_addr() -- 将IP地址从点数格式转换成无符号长整型的网络字节格式
6)inet_ntoa() -- 将IP地址从长整型转换成点数格式的字符串
四. 常用函数
序号 | 函数名 | 功能 |
---|---|---|
1 | socket() | 创建一个网络套接字,返回套接字描述符 |
2 | bind() | 为指定的套接字绑定IP地址和端口 |
3 | listen() | 让指定的套接字监听通过bind()绑定的端口 |
4 | accept() | 创建并返回一个文件描述符与listen()监听到的连接请求连接 |
5 | recv() | 用于流式套接字通讯中接收数据 |
6 | send() | 用于流式套接字通讯中发送数据 |
7 | connect() | 将指定的套接字连接到指定的IP地址和端口 |
8 | sendto() | 用于数据报套接字通讯中发送数据 |
9 | recvfrom() | 用于数据报套接字通讯中接收数据 |
10 | close() | 关闭已打开的套接字 |
11 | shutdown() | |
12 | getpeername() | |
13 | gethostname() |
4.1 socket() 函数
- 函数声明:int socket(int domain, int type, int protocol);
1)domain:协议族,决定了socket的地址类型,常用的协议族有:AF_INET、AF_INET6、
AF_LOCAL、AF_ROUTE,AF_INET决定了要用ipv4地址与端口号的组合
2)type:套接字类型,通常为SOCK_STREAM(流式类型)和SOCK_DGRAM(数据报类型)
3)protocol:指定协议,常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、
IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协
议、TIPC传输协议
- 返回值:int类型的socket描述符,错误时返回-1,并且设置全局错误变量errno
4.2 bind() 函数
- 函数声明:int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
1)sockfd:socket 返回的文件描述符
2)my_addr:指向保存着 IP 地址和本地端口的数据结构 struct sockaddr 的指针
3)addrlen:设置为 sizeof(struct sockaddr)
PS:
my_addr结构体中的sin_port 和sin_addr.s_addr需要转换为网络字节顺序
my_addr.sin_port = htons(0); // 随机选择一个没有使用的端口
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 使用自己的IP地址
- 返回值:错误时返回-1,并且设置全局错误变量errno
4.3 listen() 函数
- 函数声明:int listen(int sockfd, int backlog);
1)sockfd:socket 返回的文件描述符
2)backlog:进入队列中允许的连接数目
PS:进入的连接是在队列中等待直到调用 accept() 接受连接
- 返回值:错误时返回-1,并且设置全局错误变量errno
4.4 accept() 函数
- 函数声明:int accept(int sockfd, void *addr, int *addrlen);
1)sockfd:socket 返回的文件描述符
2)addr:指向保存着请求连接的源 IP 地址和源端口的数据结构 struct sockaddr_in 的指针
3)addrlen:指向值为 sizeof(struct sockaddr) 的局部变量 的指针
PS:通过改变 addrlen 指向的局部变量的值来说明写入到addr的字节数
- 返回值:返回一个新的套接字文件描述符,错误时返回-1,并且设置全局错误变量errno
4.5 recv() 函数
- 函数声明:int recv(int sockfd, void *buf, int len, unsigned int flags);
1)sockfd:要读的套接字描述符(socket() 或 accept() 返回的)
2)buf:指向要读的信息的缓冲的指针
3)len:缓冲的最大长度
4)flags:通常设置为0
- 返回值:返回实际读入缓冲的数据的字节数,错误时返回-1,并且设置全局错误变量errno
4.6 send() 函数
- 函数声明:int send(int sockfd, const void *msg, int len, int flags);
1)sockfd:发送数据的套接字描述符(socket() 或 accept() 返回的)
2)msg:指向发送的数据的指针
3)len:发送的数据的长度
4)flags:通常设置为0
- 返回值:返回实际发送的数据的字节数,可能小于要求发送的数目,错误时返回-1,并且设置全局错误变量errno
4.7 connect() 函数
- 函数声明:int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
1)sockfd:socket 返回的文件描述符
2)serv_addr:保存着目的端口和 IP 地址的数据结构 struct sockaddr
3)addrlen:设置为 sizeof(struct sockaddr)
PS:无需使用bind()绑定本地端口,内核将自动选择一个合适的端口号
- 返回值:错误时返回-1,并且设置全局错误变量errno
4.8 sento() 函数
- 函数声明:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
1)sockfd:socket() 返回的发送数据的套接字描述符
2)msg:指向发送的数据的指针
3)len:发送的数据的长度
4)flags:通常设置为0
5)to:指向包含了目的 IP 和端口信息的 struct sockaddr 的指针
6)tolen:通常设置为 sizeof(struct sockaddr)
PS:无需使用bind()绑定本地端口,内核将自动选择一个合适的端口号
- 返回值:返回实际发送的数据的字节数,可能小于要求发送的数目,错误时返回-1,并且设置全局错误变量errno
4.9 recvfrom() 函数
- 函数声明:
*int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int fromlen);
1)sockfd:socket() 返回的要读的套接字描述符
2)buf:指向要读的信息的缓冲的指针
3)len:缓冲的最大长度
4)flags:通常设置为0
5)from:指向包含源 IP 地址和端口信息的 struct sockaddr 的指针
6)fromlen:指向值为 sizeof(struct sockaddr) 的局部变量的指针
PS:通过改变 fromlen 指向的局部变量的值来说明写入到 from 的字节数
- 返回值:返回实际读入缓冲的数据的字节数,错误时返回-1,并且设置全局错误变量errno
4.10 close() | shutwodn() 函数
- 函数声明:close(sockfd); | int shutdown(int sockfd, int how);
1)sockfd:要关闭的套接字描述符
2)how 的值是下面的其中之一:
0 – 不允许接受
1 – 不允许发送
2 – 不允许发送和接受(和 close() 一样)
PS:
1)close() 防止套接字上更多的数据的读写。任何在另一端读写套接字的企图都将返回错误信息
2)如果在无连接的数据报套接字中使用 shutdown(),只是让 send() 和 recv() 不能使用(数据报
套接字中使用了 connect 后 是可以使用它们的)
- 返回值:返回实际读入缓冲的数据的字节数,错误时返回-1,并且设置全局错误变量errno
4.11 getpeername() 函数
- 函数声明:int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
1)sockfd:连接的流式套接字的描述符
2)addr:指向 struct sockaddr (或 struct sockaddr_in) 的指针,保存连接的另一边的信息
3)addrlen:指向值为 sizeof(struct sockaddr) 的局部变量的指针
PS:通过设置 addr,获得在连接的流式套接字上谁在另外一边
- 返回值:错误时返回 -1,设置相应的 errno
4.12 gethostname() 函数
- 函数声明:int gethostname(char *hostname, size_t size);
1)hostname:字符数组指针,在函数返回时保存主机名
2)size:hostname 数组的字节长度
PS:通过设置 hostname,获得程序所运行的机器的主机名字,然后可以使用 gethostbyname()
获得机器的 IP 地址
- 返回值:成功时返回 0,失败时返回 -1,并设置 errno
五. socket 中 TCP 连接的建立和断开
5.1 三次握手建立连接详解
- 客户端向服务器发送一个SYN x
- 服务器向客户端响应一个SYN y,并对SYN x进行确认ACK x+1
- 客户端再想服务器发一个确认ACK y+1
TCP三次握手
- 当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回
5.2 四次握手释放连接详解
- 进程调用close主动关闭连接,这时TCP发送一个FIN m
- 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认
- 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket,发送一个FIN n
- 接收到这个FIN的源发送端TCP对它进行确认
TCP四次挥手