一,创建客户端和服务端的思维导图
首先我们要知道客户端和服务端在网络中进行通信是依靠IP地址和端口号的,所以第一步就是创建一个套接字存储ip和port。通过套接字建立连接后通过read,write函数实现两者之间的交流(套接字的描述符等价于文件描述符)第一步和第二部是客户端和服务端共同的,第三步和第四步是各自的。详细流程如下
1.1 创建一个套接字
函数原型:int socket(int domain, int type, int protocol);
功能描述:创建一个套接字文件,参数不同,功能类型就会不同
参数描述:
参数 domain:网络介质,常用的有以下几种
AF_UNIX,AF_LOCAL : 本地通信协议,其实就是给域套接字用的
AF_INET:ipv4 地址协议
AF_INET6:ipv6 地址协议
最常用的就是: AF_INET
参数 type:套接字类型,有以下2个常用类型
SOCK_STREAM:提供一种有序的、可靠的、全双工的、基于连接的字节流套接字
这个描述一看就知道,tcp协议用的就是这种套接字
SOCK_DGRAM:提供一种数据报文形式的,非连接的,不可靠的,发送的数据有最大长度限制的套接字
这个描述一看就知道,udp协议使用的是这种套接字
参数 protocol: 一般直接写0
表示:网络通信协议会根据套接字的类型自动选取
如果套接字类型为 SOCK_STREAM,protocol 为0,则自动选用tcp协议
如果套接字类型为 SOCK_DGRAM,protocol 为0,则自动选用udp协议
返回值:成功创建套接字,返回套接字的描述符,失败返回-1
套接字描述符和文件描述一回事儿,遵循最小未使用原则
1.2将ip和port存入套接字
第一步 是准备一个结构体存储IP和port
该结构体结构如下:
struct sockaddr_in{
unsigned short sin_family; // 这是一个标记位,固定写 AF_INET(表面改结构体的身份)
unsigned short sin_port; // 端口号
struct in_addr sin_addr;// 一个结构体,结构如下
struct in_addr{
unsigned int s_addr; // 用来存放ip地址的变量
}
unsigned char zero[8]; // 没有实际意义,完全为了字节对齐填充大小(与 struct sockaddr结构体)
}
第二部就是将内容写入套接字
函数原型:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能描述:将写有ip和port地址信息结构体,写入套接中
参数描述:
参数 sockfd:准备写入ip和port的套接字
参数 addr:写有ip和port的结构体地址,该结构体如下
struct sockaddr {
unsigned short sa_family; // 固定写 AF_INET
char sa_data[14]; // 这个14个字节的内存里面,就是用来存放 ip 和 port的
}
如果使用这个结构体去存放ip和port的话,代码如下
参数 addrlen:就是第二个参数 addr 的实际占用长度
返回值:成功返回0,失败返回-1。bind函数非常容易报错,只要绑定的端口号已经被用了,就会报错
这个 struct sockaddr 这个结构体我们成为通用地址结构体
事实上有2个结构体会通过我们所介绍的地址上的0101直接赋值的形式转换成 struct sockaddr 这个结构体
struct sockaddr_in
struct sockaddr_un (所以struct sockaddr_in为什么会有标志位)
调用形式
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 因为本地字节序和网络字节序不一定一致,所以端口号在进入网络之前,一定要转换成大端字节序
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
if(bind(server,(struct sockaddr*)&addr,sizeof(addr)) == -1){
perror("bind"); //标记部分是类型的转换
}
第三步就是在服务端调用listen函数创建监听列表并监听客户端有没有连接,使用accept函数接受客户端的请求。
函数原型:int listen(int sockfd, int backlog);
功能描述:创建监听列表并监听客户端连接,如果有客户端连接,则将该客户端加入监听列表中
参数描述:
参数 sockfd:哪个描述符需要监听并创建监听列表,一般就是服务器的描述符
参数 backlog:监听列表的大小
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能描述:接受客户端的连接,并且获取该客户端的ip和port
参数描述:
参数 sockfd:哪个描述符接受客户端的连接,一般就是服务器
参数 addr:一个地址信息结构体的地址,该结构体中将会接受并存放新链接的客户端的ip和port
参数 addrlen:第二个参数的实际长度,但是注意这里是个指针
返回值:成功接受客户端的连接之后,会返回该客户端的套接字
客户端套接字非常有用,服务器与特定的客户端进行通信的时候,必须使用该客户端套接字
调用形式:
struct sockaddr_in client_addr = {0};
int len = sizeof(client_addr);
int client = accept(server,(struct sockaddr*)&client_addr,&len);
当然,我们也可以仅接受客户端的连接,不需要知道客户端的ip和port
int client = accept(server,NULL,NULL);
注意:accept 函数是一个阻塞型函数
也就是说,一旦调用accept函数就会立刻阻塞,直到有客户端连接为止
第四步客户端通过connect函数与服务端建立连接
函数原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能描述:根据提供的ip和port连接到指定服务器
参数描述:
参数 sockfd:客户端套接字
参数 addr:存有ip和port的一个通用地址信息结构体
参数 addrlen:第2个参数的实际长度
返回值:成功连接返回0,失败返回-1,因为存在失败的可能,所以记得判空
调用形式
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 因为本地字节序和网络字节序不一定一致,所以端口号在进入网络之前,一定要转换成大端字节序
addr.sin_addr.s_addr = inet_addr("192.168.1.1");
if(connect(server,(struct sockaddr*)&addr,sizeof(addr)) == -1){
perror("bind");
}
二.协议包的创建
2.1TCP中的粘包现象
tcp协议为了提高发送的效率,会将短时间连续发送的小数据,当做一组数据统一发送
原理是:
tcp协议本身存在一个1500字节的缓存区,tcp协议每次write发送数据的时候,总是会发送1500个字节
如果发送了n组数据,这n组数据的时间间隔很短,并且数据总大小没有超过1500个字节
那么TCP协议就会将这n组数据全都放到同一个1500字节的缓存区中去,统一发送
2.2 协议包
规定好一组数据的固定大小,以及一组数据里面每个数据占据多少个字节
然后服务器和客户端,全都遵循同样的规定实现数据的收发
这样的规定如果我们把它打包写成具体的代码,打包出来的成果我们就称为协议包
使用协议包的服务器
使用协议包的客户端