在服务器中,需要建立一个socket套接字才能对外提供一个网络通信接口,在Linux系统中套接字仅是一个文件描述符,也就是一个int类型的值
socket概念
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
我们把插头插到插座上就能从电网上获得电力供应,同样为了与远程计算机进行数据传输,需要连接到因特网,而socket就是用来连接到因特网的工具
UNIX/Linux下的socket
在UNIX/Linux下,一切都是文件
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
-
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
-
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符
Linux下socket使用
创建
#include<sys/socket.h>
int sockfd = socket(AF_INET,SOCK_STREAM,0);
-
创建socket的API中第一个参数是IP地址类型,AF_INET表示使用IPv4,如果使用IPv6请使用AF_INET6,另外AF_UNIX则是Unix域套接字,即本地套接字
-
第二个是参数为数据传输方式,SOCK_STREAM表示流格式、面向连接,多用于TCP。SOCK_DGRAM表示数据报格式、无连接,多用于UDP,至于TCP为什么为流格式,UDP为什么为面向报文,在专栏计算机网络部分为有详细解释。
-
第三个参数:协议,0表示根据前面的两个参数自动推导协议类型。设置为IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP和UDP。
sockadd_in结构体
对于客户端,服务器存在的唯一标识是一个IP地址和端口,这时候我们需要将这个套接字绑定到一个IP地址和端口上。首先创建一个sockaddr_in结构体
#include <arpa/inet.h> //这个头文件包含了<netinet/in.h>,不用再次包含了
#include<string.h> //包含了bzero,如果是c使用string.h如果是C++使用cstring.h
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
创建完后用bzero初始化这个结构体
sockaddr_in结构体内容如下:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常设置为 AF_INET
in_port_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IPv4 地址
};
其中IPV4地址结构体如下:
struct in_addr {
in_addr_t s_addr; // 32位的 IPv4 地址,采用网络字节序
};
另外如果是在IPV6或者UNIX下本地套接字的话,要使用另外的结构体
-
IPV6:
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,通常设置为 AF_INET6
in_port_t sin6_port; // 端口号,网络字节序
uint32_t sin6_flowinfo;// 流信息
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id;// 作用域ID
};
struct in6_addr {
unsigned char s6_addr[16]; // 128位的 IPv6 地址
};
-
UNIX:
struct sockaddr_un {
sa_family_t sun_family; // 地址族,通常设置为 AF_UNIX
char sun_path[108]; // Unix socket 的路径名
};
通用的sockaddr结构体
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址信息
};
-
sa_family
:-
表示地址族,通常取值为
AF_INET
、AF_INET6
、AF_UNIX
等。 -
用于确定后续的地址信息如何解释。
-
-
sa_data
:-
一个14字节的地址信息数组。
-
具体的地址信息格式取决于地址族。
-
在初始化IPV4的结构体后,就要对其设置地址族,IP地址和端口
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
绑定
然后将socket地址与文件描述符绑定:
bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
为什么定义的时候使用专用socket地址,而绑定的时候转换为通用socket地址(sockaddr),这些内容在游双《Linux高性能服务器编程》第五章第一节:socket地址API中有详细讨论,我这里也进行一部分的引用加以说明
“通用socket地址结构体显然很不好用,比如设置与获取 IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提 供了专门的socket地址结构体,同时所有专用socket地址类型的变量在实际使 用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因 为所有socket编程接口使用的地址参数的类型都是sockaddr。”
inet_addr是将char类型字符串转变为网络字节序
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); //将char类型字符串转变为网络字节序
char *inet_ntoa(struct in_addr in); //将网络字节序转换为char类型
不过现在用这些比较少,
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//af参数指明IP地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
//src参数是指向二进制IP地址的指针。
//dst是存储转换后的文本IP地址的缓冲区。
//size参数指明dst缓冲区的大小。
//该函数返回一个指向dst的指针,如果失败则返回NULL并设置errno。
int inet_pton(int af, const char *src, void *dst);
//af参数指明IP地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
//src参数是指向文本IP地址的指针。
//dst是存储转换后的二进制IP地址的缓冲区。
//该函数返回值:
//如果转换成功,返回1。
//如果src参数不是有效的IP地址字符串,返回0。
//如果出错,返回-1并设置errno。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<arpa/inet.h>
int main(){
char buf[] = "192.168.1.2"; //低字节为2,高字节为192
unsigned int name = 0;
inet_pton(AF_INET,buf,&name); //将点分十进制转换为网络字节序,大端存储,发送时用的多
printf("name = %d\n",name); //0x0201a8c0
unsigned char* p = (unsigned char*) &name;
printf("%d,%d,%d,%d\n",*p,*(p+1),*(p+2),*(p+3)); //大端存储
char ip[16] = "";
inet_ntop(AF_INET,&name,ip,16); //将网络字节序转换为点分十进制 ,接收时用的多
printf("%s",ip);
}
打印结果为:
name = 33663168 192,168,1,2 192.168.1.2
监听
使用listen
函数监听这个socket端口,这个函数的第二个参数是listen函数的最大监听队列长度,系统建议的最大值SOMAXCONN
被定义为128。
listen(sockfd, SOMAXCONN);
要接受一个客户端连接,需要使用accept
函数。对于每一个客户端,我们在接受连接时也需要保存客户端的socket地址信息,于是有以下代码:
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len = sizeof(clnt_addr);
bzero(&clnt_addr, sizeof(clnt_addr));
int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
printf("new client fd %d! IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
要注意和accept
和bind
的第三个参数有一点区别,对于bind
只需要传入serv_addr的大小即可,而accept
需要写入客户端socket长度,所以需要定义一个类型为socklen_t
的变量,并传入这个变量的地址。另外,accept
函数会阻塞当前程序,直到有一个客户端socket被接受后程序才会往下运行。
现在,客户端已经可以通过IP地址和端口号连接到这个socket端口了,让我们写一个测试客户端连接试试:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
代码和服务器代码几乎一样:创建一个socket文件描述符,与一个IP地址和端口绑定,最后并不是监听这个端口,而是使用connect
函数尝试连接这个服务器。
运行编译出来的./server和./client可以看到服务器接收到了客户端的连接请求,并成功连接
new client fd 3! IP: 127.0.0.1 Port: 53505