socket是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
UNIX/Linux 中的一切都是文件,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
通过 socket() 函数可以创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。剩下的操作就是对这个文件的操作了
这个世界上有很多种套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字),Internet套接字用于网络上两个主机之间的交互,Unix套接字用于单个主机上两个进程之间的交互,这次就总结一下Internet套接字和Unix套接字的基本的编程过程,对它们做一下对比
Internet套接字:
Internet套接字主要有两种传输方式SOCK_STREAM和SOCK_DGRAM
流格式套接字SOCK_STREAM 有以下几个特征:
数据在传输过程中不会消失;
数据是按照顺序传输的;
数据的发送和接收不是同步的( “不存在数据边界”)
SOCK_STREAM传输层使用的是TCP协议,TCP 协议会控制数据按照顺序到达并且没有错误。
数据的发送和接收不是同步的是指:流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。
数据报格式套接字SOCK_DGRAM主要有以下几个特征:
强调快速传输而非传输顺序;
传输的数据可能丢失也可能损毁;
限制每次传输的数据大小;
数据的发送和接收是同步的(有的教程也称“存在数据边界”)
数据报套接字使用 UDP 协议,应用层的数据会被分割成多个数据包,每个数据包独立发送,并且每个数据包不能超过数据包大小的限制,在数据包套接字中数据的发送和接收是同步的,发送端发送过来一个数据包,接收端就会接收这个数据包
服务端:
1. 创建socket
|
int socket(int af, int type, int protocol); af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6 type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM和SOCK_DGRAM protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP |
socket函数执行成功后会返回一个文件描述符作为后边bind函数的入参,用于和ip地址和端口号进行绑定
2. bind函数将套接字与特定的 IP 地址和端口绑定起来
|
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); sock 为 socket 文件描述符 addr 为 sockaddr 结构体变量的指针 addrlen 为 addr 变量的大小
sockaddr_in结构体 struct sockaddr_in{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 uint16_t sin_port; //16位的端口号 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 }; |
下面的代码,将创建的套接字与IP地址 127.0.0.1、端口 1234 绑定
|
//创建套接字 int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建sockaddr_in结构体变量 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); //端口 //将套接字和IP、端口绑定 bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); |
其中inet_addr函数用于将ipv4地址的字符串形式转换为uint32_t形式,同样的还有int inet_pton(int af, const char *src, void *dst)函数,这个函数通过指定地址族可以实现ipv4和ipv6的字符串ip地址的转换,sockaddr_in的第三个成员还是是一个结构体,这个结构体只有一个成员变量
|
struct in_addr{ in_addr_t s_addr; //32位的IP地址 in_addr_t实际就是uint32_t类型 }; |
htons()函数则用于主机序向网络序的转换,sockaddr_in结构体设置完成后需要对其进行类型转换才能使用bind函数将这个结构体和socket返回的文件描述符进行绑定,转换的类型是sockaddr结构体
|
struct sockaddr{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 char sa_data[14]; //IP地址和端口号 }; |
sockaddr_in和sockaddr结构体的大小是相同的
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体,另外还有 sockaddr_in6,用来保存 IPv6 地址,后边也就sockaddr_un结构体用于保存unixsocket通信的文件路径,在绑定的时候都是需要强制转换的,通过强制转换的方式可以保证bind函数的通用性,不用根据不同的地址结构体类型设置不同的函数,只需要将sockaddr结构体作为入参即可,有点像面向对象编程里多态的概念
3. listen监听客户端的连接
通过listen()函数可以让套接字进入被动监听状态
|
int listen(int sock, int backlog); sock为socket()函数返回的文件描述符,backlog为监听队列的长度 |
被动监听是指套接字会等待客户端连接,监听响应的端口,但是这不会阻塞程序的运行,通过backlog可以设置请求队列的长度,当套接字正在处理客户端请求的时候,如果有新的请求到来,套接字是暂时无法处理的,会先把请求放到这个队列中,按照客户端请求到来的顺序依次对它们进行处理
4.accept()函数接收客户端的请求
listen()函数调用后,套接字处于监听状态,通过accept()函数就可以接受客户端发来的请求
|
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); sock为socket()函数返回的文件描述符 addr参数是我们需要传入的sockaddr_in结构体,这个结构体会被accept()函数赋值成发来请求的客户端的ip地址和端口号 addrlen为参数addr结构体的长度 |
accept函数会返回一个新的套接字来和客户端进行通信,accept函数执行后程序会被阻塞,一直等待客户端的连接并处理客户端的请求
5. send()/write()和recv()/read()函数来收发数据
通过write()/send()函数可以发送数据
|
ssize_t write(int fd, const void *buf, size_t nbytes); ssize_t send(int fd, const void *buf, size_t nbytes, int flags); 这两个函数的前三个参数是一致的,fd 为要写入的文件的描述符,对于服务端,这里的fd是服务端accept函数返回的新的文件描述符,buf 为要写入的数据的缓冲区地址,nbytes 为buf的字节数,于write函数相比,send函数会多一个flags参数,flags 为发送数据时的选项 |
这两个函数执行成功会返回写入的字节数,失败则会返回-1
通过read()/recv()函数可以接收数据
|
ssize_t read(int fd, void *buf, size_t nbytes); int recv(int fd, void *buf, size_t nbytes, int flags); 前三个参数同样是一样的,fd 为要读取的文件的描述符,对于服务端fd是accept函数返回的文件描述符,buf 为要接收数据的缓冲区地址,nbytes 为buf的字节数,recv函数比read函数多了一个flags参数,flags 为发送数据时的选项 |
这两个函数执行成功会返回读取的字节数,失败则会返回-1
服务端例子
|
#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(){ //1. 创建套接字 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));
//2. 进入监听状态,等待用户发起请求 listen(serv_sock, 20);
//3. 接收客户端请求 struct sockaddr_in clnt_addr; socklen_t clnt_addr_size = sizeof(clnt_addr); int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据 char str[] = "Hello world!"; write(clnt_sock, str, sizeof(str));
//关闭套接字 close(clnt_sock); close(serv_sock);
return 0; } |
客户端:
1. 创建socket
|
int socket(int af, int type, int protocol); af 为地址族(Address Family) type 为数据传输方式/套接字类型 protocol 表示传输协议 创建的时候和服务端创建的时候一样 |
2. connect函数连接服务端
connect函数用来创建新的连接
|
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 这个函数的参数类型和服务端的bind函数一样 sock为客户端的socket函数返回的文件描述符 serv_addr为sockaddr_in结构体,这个结构体要设置成服务端的ip地址和端口号,并且需要做强制类型转换 addrlen为sockaddr_in结构体的长度 |
3. send()/write()和recv()/read()函数来收发数据
通过write()/send()函数可以发送数据
|
ssize_t write(int fd, const void *buf, size_t nbytes); ssize_t send(int fd, const void *buf, size_t nbytes, int flags); 这两个函数的前三个参数是一致的,fd 为要写入的文件的描述符,对于客户端,这里的fd是socket函数的文件描述符,buf 为要写入的数据的缓冲区地址,nbytes 为buf的字节数,于write函数相比,send函数会多一个flags参数,flags 为发送数据时的选项 |
这两个函数执行成功会返回写入的字节数,失败则会返回-1
通过read()/recv()函数可以接收数据
|
ssize_t read(int fd, void *buf, size_t nbytes); int recv(int fd, void *buf, size_t nbytes, int flags); 前三个参数同样是一样的,fd 为要读取的文件的描述符,对于客户端fd是socket函数返回的文件描述符,buf 为要接收数据的缓冲区地址,nbytes 为buf的字节数,recv函数比read函数多了一个flags参数,flags 为发送数据时的选项 |
这两个函数执行成功会返回读取的字节数,失败则会返回-1
客户端例子
|
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h>
int main(){ //1. 创建套接字 int sock = socket(AF_INET, SOCK_STREAM, 0);
//2. connec函数向服务器(特定的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));
//3. 读取服务器传回的数据 char buffer[40]; read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//关闭套接字 close(sock);
return 0; } |
现在来看一下unix socket通信,UNIX Domain SOCKET 是在Socket架构上发展起来的用于同一台主机的进程间通信(IPC)。它不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序列号应答等。只是将应用层数据从一个进程拷贝到另一个进程。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
服务端:
1. 开始创建socket
|
int socket(int domain, int type, int protocol) domain(域) : AF_UNIX type : SOCK_STREAM/ SOCK_DGRAM protocol : 0 socket函数成功后会返回socket的文件描述符 关于两种type: SOCK_STREAM(流) : 提供有序,可靠的双向连接字节流。 可以支持带外数据传输机制, 无论多大的数据都不会截断 SOCK_DGRAM(数据报):支持数据报(固定最大长度的无连接),数据报超过最大长度,会被截断. |
2. bind函数将socket的文件描述符绑定到一个socket类型的文件上
|
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); sockfd : 传入sock的文件描述符 addr : 用sockaddr_un表示 addrlen : 结构体长度 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ }; |
3. listen监听客户端的连接
|
int listen(int sockfd, int backlog); sockfd : 文件描述符 backlog : 连接队列的长度 |
4. accept接受客户端的连接
|
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len); Unux domain socket不存在客户端地址的问题,因此这里的addr和addrlen参数可以设置为NULL |
服务端例子
|
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/stat.h> #include<sys/socket.h> #include<sys/types.h> #include<sys/un.h> #include<errno.h> #include<stddef.h> #include<unistd.h> #define MAX_CONNECT_NUM 2 #define BUFFER_SIZE 1024 const char *filename="tmp";
int main() { int fd,new_fd,len,i; struct sockaddr_un un; // 1. 创建socket并返回文件描述符 fd = socket(AF_UNIX,SOCK_STREAM,0);// 设置地址族和报文类型,协议值为0 if(fd < 0){ printf("Request socket failed!\n"); return -1; } // 设置sockaddr_un结构体 地址族设置成UNIX文件路径设置成socket通信用到的文件路径 un.sun_family = AF_UNIX; unlink(filename); strcpy(un.sun_path,filename); //2. 将socket创建的文件描述符和sockaddr_un结构体绑定 if(bind(fd,(struct sockaddr *)&un,sizeof(un)) <0 ){ printf("bind failed!\n"); return -1; } //3. listen函数开始监听客户端,并设置队列最大长度 if(listen(fd,MAX_CONNECT_NUM) < 0){ printf("listen failed!\n"); return -1; } while(1){ char buffer[BUFFER_SIZE]; bzero(buffer,BUFFER_SIZE); //4. accept函数接受客户端连接,accept函数会阻塞程序,成功后返回一个新的文件描述符 new_fd = accept(fd,NULL,NULL); if(new_fd < 0){ printf("accept failed\n"); return -1; } //5. 用recv/read函数从新返回的文件描述符里获取数据放入到buffer中 int ret = recv(new_fd,buffer,BUFFER_SIZE,0); //int ret = read(new_fd,buffer,BUFFER_SIZE); if(ret < 0){ printf("recv failed\n"); } printf("%s\n",buffer); close(new_fd); break; } close(fd); } |
客户端:
1. 创建socket
|
int socket(int domain, int type, int protocol) domain(域) : AF_UNIX type : SOCK_STREAM/ SOCK_DGRAM protocol : 0 和服务端创建socket一样 |
2. connet函数连接服务端
|
int connect(SOCKET s, const struct sockaddr * name, int namelen) s:socket函数返回的文件描述符 name:sockaddr_un结构体 namelen:sockaddr_un结构体的长度 sockaddr_un结构体sun_family设置成AF_UNIX,sun_path字段设置成用于socket通信的文件的路径 |
客户端例子
|
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/stat.h> #include<sys/socket.h> #include<sys/types.h> #include<sys/un.h> #include<errno.h> #include<stddef.h> #include<unistd.h> #define BUFFER_SIZE 1024 const char *filename="tmp";
int main() { struct sockaddr_un un; int sock_fd; char buffer[BUFFER_SIZE] = "Hello world!"; // 1. 设置sockaddr_un结构体并创建socket un.sun_family = AF_UNIX; strcpy(un.sun_path,filename); sock_fd = socket(AF_UNIX,SOCK_STREAM,0); if(sock_fd < 0){ printf("Request socket failed\n"); return -1; } // 2. connect函数连接服务端,需要传入创建的socket和sockaddr_un结构体 if(connect(sock_fd,(struct sockaddr *)&un,sizeof(un)) < 0){ printf("connect socket failed\n"); return -1; } //3. send/write函数将buffer里的信息发送出去 send(sock_fd,buffer,BUFFER_SIZE,0); //write(sock_fd,buffer,BUFFER_SIZE,0); close(sock_fd); return 0; } |
通过对比可以看到无论是internet socket还是unix socket,服务端和客户端创建socket并进行通信的步骤都是一致的
服务端: socket -> bind -> listen -> accept -> write/send或read/recv -> close
客户端: socket -> connect -> write/send或read/recv -> close
并且每一步骤调用的函数也都是同样的函数,不同点主要在于
1. socket函数domain字段的值不同,internet socket值为AF_INET,unix socket值为AF_UNIX
2.socket函数的type参数的值都是SOCK_STREAM和SOCK_DGRAM,但是对于unix socket通信来说,由于是进程间的通信,这两种类型提供的服务都是可靠的
3.bind函数中,internet socket传入的结构体是sockaddr_in,并且会绑定ip地址和端口号,unix socket传入的结构体是sockaddr_un,会绑定一个用于进程间通信的文件,但是都是需要进行强制转换的
4.accept函数中,internet socket需要传入sockaddr_in结构体来保存发出请求的客户端的地址,但是在unix socket中,这一项为NULL,在进程通信中,在服务端上无法确定出来这个客户端进程是哪一个
后边的收发数据的调用过程都是一样的,总体来看,两种socket通信的步骤是一致的,但是用于是不一样的,一个是用于网络中两个主机之间的通信,另一个则是一个主机中两个进程之间的通信,这种一致性我觉得可能是因为linux系统中的一切皆文件的思想,无论是进程间通信或者是主机间通信,通过socket函数就能返回一个文件描述符,这个文件描述符的背后可能是用于网络传输的网络连接,或者是进程间的通信,但是对于编程者而言就是用于输入输出的一个文件描述符,这样进程间的通信和主机间的通信都被抽象成了对一个文件的输入输出操作,所以才造成了internet socket和unix socket通信的这种一致性;在细节方面,也正是因为有bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)函数,它提供了一个同一的结构体,在不同类型的socket编程时可以让sockaddr_un和sockaddr_in结构体强制类型转换,所以能够让编写不同类型的socket程序的时候可以调用同样的接口,才造成了在编程时候的这种一致性
来源:https://blog.csdn.net/weixin_39286923/article/details/100015860