前言
socket(套接字),socket的英文翻译为插座。在通信过程中,套接字必须是成对(指客户端和服务器端都要创建套接字)出现的,就想插头与插座的关系一样。
在Linux环境下,socket用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字通信原理如下图所示:
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
socket模型创建流程图
相关函数
备注:本篇博客介绍函数相关参数只介绍我们平时最常用的参数
socket函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
作用:创建一个套接字
参数:
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
bind函数
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:给socket绑定一个地址结构(IP+PORT),用于绑定服务器的地址结构
参数:
sockfd:
socket文件描述符,即socket函数的返回值
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)
返回值:
成功返回0,失败返回-1, 设置errno
注意bind函数的第2参数addr就是服务器的相关地址结构,因此需要我们手动给出。一般服务器端addr(这里写作serv)的构造方式一般如下:
struct sockaddr_in serv;
memset(&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_port=htons(9876);
serv.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR_ANY表示取出系统中有效的任意IP地址
客户端addr(这里写作serv)的构造方式一般如下:
struct sockaddr_in serv;
memset(&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);//转换字符串到网络地址
另外注意到addr定义时,其类型为struct sockaddr_in;而bind函数的第2参数是struct sockaddr类型,所以我们在传参的时候要强制转换一下。至于为什么要设计这两个结构,其实原因就在于这些网络编程函数诞生早于IPv4协议,早期这些函数使用的都是sockaddr结构体,而sockaddr_in是基于IPv4协议的,所以为了兼容,我们需要进行强制转换。我们可以发现sockaddr_in和sockaddr其实大小一样,都是16字节。其实,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6(IPv6,这个结构体大小28字节),由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
listen函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
作用:设置同时与服务器连接的上限数。(同时进行3次握手的客户端数量)
参数:
sockfd:
socket文件描述符,即socket函数的返回值
backlog:
设置同时与服务器连接的上限数。最大值为128
返回值:
成功返回0,失败返回-1,设置errno
注意:listen函数并不负责监听客户端的连接请求,其作用只是设置同时与服务器连接的客户端的数量,因此其是不阻塞的,真正的阻塞发生在accept阶段,它才负责监听客户端的连接请求,所以会发生阻塞。
accept函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用:阻塞等待客户端建立连接
参数:
sockfd:
socket文件描述符,即socket函数的返回值
addr:
传出参数,返回成功与服务器建立连接的客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
注意,在服务器端共建立了两个socket文件描述符。分别是使用socket函数创建的一个socket文件描述符(监听描述符listenfd)和使用accept创建的文件描述符(已连接描述符connfd)。监听描述符listenfd的作用是在accept函数中等待来自客户端的连接请求到达监听描述符。而已连接描述符connfd就专门用来与客户端建立通信。形象的比喻就是listenfd就像酒店的迎宾小姐,客户端就相当于客人,connfd就是酒店为每个客人配备的服务人员,当迎宾小姐迎接到客人之后,她就不管这个客人了,转而去迎接其他客人,进入酒店的这个客人转而与酒店为其配备的服务人员进行交流(类比客户端与服务器的通信)。
connect函数
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:使用现有的socket与服务器建立连接
参数:
sockfd:
socket文件描述符,即socket函数的返回值
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
注意:客户端自己并没有像服务器一样使用bind函数绑定客户端地址结构,其是系统采用的"隐式绑定",也就是操作系统自动帮我们绑定好了。
TCP通信流程分析
server:
- socket() 创建socket
- bind() 绑定服务器地址结构
- listen() 设置监听上限
- accept() 阻塞监听客户端连接
- read(fd) 读socket获取客户端数据
- 根据读到的数据进行相应操作
- write(fd) 将数据发送给客户端
- close() 关闭连接
client:
- socket() 创建socket
- connect() 与服务器建立连接
- write() 写数据到socket
- read() 读取服务器发来的响应
- 显示读取结果
- close 关闭连接