目录
1、linux文件描述符
在linux中,一切都是文件。一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件。例如,stdin 称为标准输入文件,它对应的硬件设备一般是键盘,stdout 称为标准输出文件,它对应的硬件设备一般是显示器。对于所有的文件,都可以使用 read() 函数读取数据,使用 write() 函数写入数据。
“一切都是文件”的思想极大地简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样。所有在Linux中创建的文件都有一个 int 类型的编号,称为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可以。例如,stdin 的描述符为 0,stdout 的描述符为 1。
在Linux中,socket 也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件 I/O 相关的函数。可以认为,两台计算机之间的通信,实际上是两个 socket 文件的相互读写。
文件描述符有时也被称为文件句柄(File Handle),但“句柄”主要是 Windows 中术语。
2、在Linux下创建 socket
在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:
int socket(int af, int type, int protocol);
- af:地址族(Address Family),也就是IP地址类型,常用的有AF_INET和AF_INET6。AF是"Address Family"的简写,INET是"Internet"的简写。AF_INET表示IPv4地址。例如127.0.0.1;AF_INET6表示IPv6地址,例如1030::C9B4:FF12:48AA:1A2B。你也可以使用PF前缀,PF是"Protocol Family"的简写,它和AF是一样的。例如,PF_INET等价于AF_INET,PF_INET6等价于AF_INRT6。
- type:数据传输方式,常用的有SOCK_STREAM和SOCK_DGRAM。
- protocol表示传输协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP传输协议和UDP协议。
看到这儿,你可以能有个疑问,有了IP地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?
没错,一般情况下有了af和type两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同额协议支持同一种IP地址类型和数据传输方式。如果我们不知名使用哪种协议,操作系统是没办法自动推演的、
如果af的值设置为PF_INET,使用SOCK_STREAM传输方式,那么满足这两个条件的协议只有TCP,因此可以这样来调用SOCK()函数:
int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP); //TCP套接字
如果af的值设置为PF_INET,使用SOCK_DGRAM传输方式,那么满足这两个条件的协议只有UDP,因此可以这样来调用SOCKET()函数:
int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字
上面的两种情况都只有一种协议满足条件,可以将protocol的值设为0,系统会自动推演出应该使用什么协议,代码如下所示
int tcp_socket = socket(AD_INET,SOCK_STREAM,0); //TCP套接字
int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字
3、bind()函数和connect()函数
socket()函数用来创建套接字,确定套接字的各种属性,然后服务器端要用bind()函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字;而客户端要用connect()函数建立连接。
3.1、bind()函数
bind()函数的原型为:
int bind(int sock,struct sockaddr *addr,socklen_t addrlen);
sock为socket文件描述符,addr为sockaddr结构体变量指针,addrlen为addr变量的大小。
我们来看一个代码,将创建的套接字与IP地址128.0.0.1、端口1123绑定:
int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO); //创建一个TCP套接字
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123); //端口
bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));
我们来看一下sockaddr_in结构体
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
unit16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
}
- sin_family和socket()的第一个参数的含义相同,取值也要保持一致。
- sin_port为端口号,uint16_t的长度为两个字节,理论上端口号的取值范围为0~65536,但0~1023的端口一般由系统分配给特定的服务程序,例如web服务的端口为70,FTP服务的端口为21,所以我们的程序尽量在1024~65536之间分配端口号。
- sin_addr是struct in_addr结构体类型的变量。
- sin_zero是多于的8个字节,没有用,一般使用memset()函数填充为0。上面的代码中,先用memset()将结构体的全部字节填充为0,再给前三个成员赋值,剩下的sin_zero自然就是0了。
in_addr结构体
sockaddr_in的第三个成员是in_addr类型的结构体,该结构只包含一个成员,如下所示:
struct in_addr
{
in_addr_t s_addr; //32位的IP地址
};
in_addr_t在头文件<netinet/in.h>中定义,等价于unsigned long,长度为四个字节。也就是说,s_addr是一个整数,而IP地址是一个字符串,所以需要inet_addr()函数进行转换,例如:
unsigned long ip = inet_addr("127.0.0.1");
运行结果:
为什么使用sockaddr_in而不使用sockaddr?
bind()第二个参数的类型为sockaddr,而代码中却使用sockaddr_in,然后再强制转换为sockaddr。
sockaddr结构体的定义如下:
struct sockaddr
{
sa_family_t sin_family; //地址族
char sa_data[14]; //IP地址和端口号
}
下图是sockaddr与sockaddr_in的对比(括号中的数字表示所占用的字节数)
sockaddr和sockaddr_in的长度相同,都是16个字节,但是sockaddr的sa_data区域需要同时指定IP地址和端口号,例如"127.0.0.1:8080",遗憾的是没有相关函数将这个字符串转换成需要的形式,也就很难给sockaddr类型的变量直接赋值,所以使用sockaddr_in来代替。这两个结构体的长度相同,强制转换类型时也不会丢失字节,也没有多于的字节。
可以认为,sockaddr是一个通用的结构体,可以用来保护多种类型的IP地址和端口号,而sockaddr_in是专门用来存IPv4的结构体。另外还有sockaddr_in6,用来保存IPv6地址,它的定义如下:
struct aockaddr_in6
{
sa_family_t sin6_family; //IP地址类型,取值为AF_INET6
in_port_t sin6_port; //16位端口号
uint32_t sin6_flowinfo; //IPv6流信息
struct in6_addr sin6_addr; //具体的IPv6地址
unit32_t sin6_scpoe_id; //接口范围ID
};
3.2、connect()函数
connect()函数用来建立连接,它的原型为:
int connect(int sock,struct sockaddr *serv_addr,struct sockaddr*serv_addr,socklen_t addrlen);
各个参数的说明与bind()函数相同。
4、listen()函数和accept()函数
对于服务器端程序,使用bind()绑定套接字后,还需要使用listen()函数让套接字进入被动监听状态,再调用accept()函数,就可以随时响应客户端的请求了。
4.1、listen()函数
通过listen()函数可以让套接字进入被动监听状态,它的原型为:
int listen(int sock,int backlog)
sock为需要进入监听状态的套接字,backlog为请求队列的最大长度
所谓被动监听,是指在没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它先放到缓冲区中,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列。
缓冲区的长度(能存放多少个客户端的请求)可以通过listen()函数的backlog参数指定,但究竟为多少并没有什么标准,根据你的需求来定。
如果将backlog的值设置为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百或者更多。
当请求队列满时,就不再接收新的请求,对于linux,客户端会受到ECONNREFUSED错误。
注意:listen()函数只是让套接字处于监听状态,并没有接收请求。接收请求需要使用accept()函数来阻塞进程执行,直到有新的请求到来。
4.2、accept()函数
当套接字处于监听状态时,可以通过acceot()函数来接收客户端请求。它的原型为:
int accept(int socket,struct sockaddr *addr,socklen_t *addrlen);
它的参数与listen()和connect()是相同的;sock为服务器端套接字,addr为sockaddr_in结构体变量,addrlen为参数addr的长度,可以由sizeof()求得。
accept()返回一个新的套接字和客户端通信,addr保存了客户端的IP地址和端口号,而sock是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
5、write()和read()
linux不区分套接字文件和普通文件,使用write()可以向套接字中写入数据,使用read()可以从套接字中读取数据。
两台计算机之间得通信相当于两个套接字之间得通信,在服务端用write()向套接字写入数据,客户端就能收到,然后再使用read()从套接字中读取出来,就完成了一次通信。
5.1、write()函数
write()的原型为:
/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int fd,const void *buf,size_t nbytes);
write()函数会将缓冲区buf中的nbytes个字节写入文件fd,成功则返回写入的字节数,失败返回-1。
5.2、read()函数
/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int fd,void *buf,size_t nbytes);
read()函数会从fd文件中读取nbytes个字节并保存在缓冲区buf中,成功则返回读取到的字节数(遇到文件结尾返回0),失败则返回-1。
6、一个service和client的简单实现
/*================================================================
* Copyright (C) 2021 baichao All rights reserved.
*
* 文件名称:service.c
* 创 建 者:baichao
* 创建日期:2021年01月22日
* 描 述:
*
================================================================*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//进入监听状态,等待用户发起请求
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
while(1)
{
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据
char str[] = "不要艾特我";
write(clnt_sock, str, sizeof(str));
//关闭套接字
close(clnt_sock);
}
close(serv_sock);
return 0;
}
/*================================================================
* Copyright (C) 2021 baichao All rights reserved.
*
* 文件名称:client.cpp
* 创 建 者:baichao
* 创建日期:2021年01月22日
* 描 述:
*
================================================================*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//读取服务器传回的数据
char buffer[40];
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//关闭套接字
close(sock);
return 0;
}
运行结果:
启动server
server处于监听状态
启动客户端:
至此,一个简易的socket通信代码完成