目录
在前面所介绍的几种进程间通信(信号、管道)中所通信的进程都需要在一台计算机上,而socket域进行的通信方式,不仅可以在一台计算机,而且可以是通过网络所连接的不同计算机,这种方式称为 命名socket或者UNIX域socket,这也是一种进程间通信的方式,UNIX域数据报服务是可靠的,不会传递出错也不会丢失信息。其提供两类套接字: 字节流套接字(stream)和数据报套接字(datagram)。
socket起源于UNIX,在UNIX下一切皆文件,而socket是一种“打开——读/写——关闭”的模式,服务器端和客户端各自维护一个文件,在建立连接后,通过对文件描述符的操作可以向自己文件写入内容供对方读取或者读取对方内容,将文件描述符关闭意味着通讯结束。
在介绍socket编程前首先我们要了解下 TCP/IP协议集:
1、TCP/IP协议集
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机间如何连入因特网及数据在主机间传输的标准。
TCP/IP协议不单单是指TCP和IP的协议而是指整个因特网上TCP/IP协议族。不同于ISO的七层网络模型,TCP/IP协议参考模型是将TCP/IP系列协议抽象到四个层中:如下图所示:
我们需要了解socket在TCP/IP协议参考模型中对应那一层,能够更好地了解到其特点:
通过上图我们可了解到socket对应在模型中的应用层和传输层之间的一个抽象层,他把两层之间的复杂操作抽象为几个简单的接口,从而实现进程间在网络上的通信。
2、socket套接字
用户认为两台主机间的信息传递只是建立在应用程序上,但从计网的角度而言,实际上在TCP连接中是靠套接字来传递信息。
对于TCP而言,用主机的IP地址加上应用程序对应的端口号作为TCP连接的端点,这种端点叫做socket套接字或插口,用(IP地址:端口号)来表示,区分不同应用程序进程间的网络通信和连接,主要通过三个参数:通信的目的IP、使用传输层的协议(TCP或UDP)和使用的端口号。
3、socket流程
从上述流程图可知:
- 服务器先根据地址类型(ipv4、ipv6),socket类型和选择协议创建socket(文件描述符)。
- 服务器端绑定客户端的IP地址及端口号。
- 服务器端监听端口号,等待客户端发送连接请求。服务器端socket此时未打开。
- 客户端创建socket后打开,根据服务器的ip地址和端口号尝试连接服务器socket。
- 服务器socket接收到客户端socket请求,直到客户端返回连接信息后,服务器端socket进入阻塞状态,即accept等待客户端发送信息后返回。
- 客户端连接成功后向服务器发送连接状态信息。
- 服务器accept返回连接成功。
- 客户端写入数据后服务器端进行读取信息。
- 客户端关闭套接字,服务器端也关闭。
4、socket编程API
首先根据服务器端的从上至下的顺序来介绍:
4.1、socket()创建描述符
#include <sys/socket.h> //头文件
int socket(int domain, int type, int protocol); //原型
//成功时返回非负数
socket函数对应于普通文件的打开操作,普通文件的打开操作返回一个文件描述符,而socket()
为了创建一个socket描述符,唯一标识一个socket,后续通过对其创建为socket描述符进行读写操作,从上述流程可看出客户端或者服务器端都需要socket()来创建socket描述符。
参数介绍:
- domain :协议域,又可称为协议族(family)。
常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等(AF是address family简称)。协议族决定了socket的地址类型,通常AF_INET(IPv4)、AF_INET6(IPv6). - type:指定socket类型。
常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。主要SOCK_STREAM(TCP协议)、SOCK_DGRAM(UDP协议)。 - protocol:指定协议。
常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。 当protocol为0时,会自动选择type对应的默认协议,一般为 0。
4.2、bind()绑定描述符
当我们使用socket()
来创建socket描述符时,返回的描述符信息存在于协议族中(address family, AF_XXX)空间中,没有一个具体的地址,因此我们需要借助bind()
函数将地址族中特定的地址赋给socket,服务器启动时会绑定一个特有地址(ip地址+端口号),这样客户端可通过该地址访问服务器,而客户端就可以由系统内分配一个端口和其自身的ip地址组合(connect()
时分配)。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数介绍:
- sockfd:sockfd描述字,通过
socket()
创建的唯一标识。 - addrlen:对应的地址长度。
- addr:是一个const struct sockaddr *类型的指针,指向要绑定给sockfd的协议地址,该地址结构会根据socket创建时的地址协议族的不同而不同,但最后会统一强转赋值给sockaddr类型的指针传给内核:
通用套接字sockaddr结构定义:
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
ipv4对应的是sockaddr_in类型定义:
struct in_addr {
uint32_t s_addr;
};
struct sockaddr_in {
unsigned short sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
uint16_t sin_port; /* 2 bytes port*/
struct in_addr sin_addr; /* 4 bytes IPv4 address*/
unsigned char sin_zero[8]; /* 8 bytes unused padding data, always set be zero */
};
ipv6对应的sockaddr_in6类型定义:
struct in6_addr{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
}__in6_u;
}
struct sockaddr_in6{
unsigned short sin6_family; /*2B*/
uint16_t sin6_port; /*2B*/
uint32_t sin6_flowinfo; /*4B*/
struct in6_addr sin6_addr; /*16B*/
uint32_t sin6_scope_id; /*4B*/
};
上述几种协议地址的结构体定义示意图:
从上图中可看出通用套接字地址结构体(struct sockaddr)和ipv4套接字地址结构体(struct sockaddr_in
)大小都为16个字节,因为其开头一样,内存大小也一样,因此强转后可直接使用,而ipv6的套接字结地址构体为28个字节大小,但bind()
函数一共有三个参数,第一个为监听的文件描述符,第二个为sockaddr*类型,第三个为传入指针原结构体大小,因此我们只需要直到后两个信息后无论传进来的结构体是什么类型,因为头都一样,每个不同协议格式地址的前两个字节都会转为sa_family,在和首地址就能得到我们想要的信息,制语sockaddr 中的sa_data 具体是多少我们不用考虑。
在数据传输的过程中会涉及到字节序的转换问题,在这篇博客中有介绍:主机序和网络序及其转换函数
4.3、listen()监听描述符
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而变成一个服务器程序。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
参数介绍:
- sockfd:即要监听的socket描述字。
- backlog:相应socker可以在内核里排队的最大连接数。
4.4、accept()接受连接
TCP服务器端依次调用socket()
、bind()
、listen()
之后,就可以监听指定的socket地址了。 服务器之后会通过调用accept()
接收客服端的连接请求,该函数默认为一个阻塞函数,知道有客户端发来请求才会返回,当客户端发来connect()
请求时会触发accept的返回,从而建立TCP连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数介绍:
- **sockfd:**服务器端所监听的socket描述符。
- **addr:**客户端的协议地址;客户端请求链接时候,肯定会发送自己的地址,以便服务器端能够知道链接的目的地(参见connect函数参数);
- **addrlen:**协议地址的长度。
accept的返回值是由内核自动分配的一个新的描述符,代表返回与客户端连接成功,可对该描述符进行操作,可调用write()
向其写内容,或者调用read()
读取客服端发来的信息,调用close()
则于客户端断开连接。
4.5、connect()连接函数
客户端在通过socket()
创建好描述符后就可调用connect()
来连接服务器,此时服务器端的accept会接受这个请求并返回一个连接描述符,而客户端的connect也会返回一个连接描述符,即可通过对两个描述符的操作进行通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:客户端的socket描述字;
- addr:要链接的服务器socket地址信息,包含服务器的ip和端口等信息。
- addrlen:socket地址长度。
4.5、close()关闭函数
在服务器端与客户端建立连接后,会进行一些读写操作,完成读写后要关闭相应的socket描述字,例如打开关闭文件一样。
int close(int fd);
close后该描述字不能再由调用进程使用。
5、示例代码
服务器端:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_PORT 8889
#define BACKLOG 13
int main(int argc,char **argv)
{
int rv = -1;
int listen_fd, client_fd = -1;
struct sockaddr_in serv_addr;
struct sockaddr_in cli_addr;
socklen_t cliaddr_len;
char buf[1024];
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if(listen_fd < 0)
{
printf("创建socket失败 : %s\n", strerror(errno));
return -1;
}
printf("socket 创建 fd[%d]\n", listen_fd);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("创建socket失败: %s\n", strerror(errno));
return -2;
}
printf("socket[%d] 绑定所有IP地址的端口[%d]就绪 \n", listen_fd, LISTEN_PORT);
listen(listen_fd, BACKLOG);
while(1)
{
printf("\n准备接受客户端的连接请求...\n");
client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cliaddr_len);
if(client_fd < 0)
{
printf("接收请求失败 :%s\n",strerror(errno));
return -3;
}
printf("接受新的客户端[%s:%d] 的描述符: [%d]\n", inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port), client_fd);
memset(buf, 0, sizeof(buf));
if((rv=read(client_fd, buf, sizeof(buf))) < 0)
{
printf("从客户端中读取数据[%d] 失败 :%s", client_fd, strerror(errno));
close(client_fd);
continue;
}
else if( rv ==0 )
{
printf("客户端[%d]没有连接\n", client_fd);
close(client_fd);
continue;
}
printf("从客户端[%d]读取 %d 个字节的数据:'%s'\n", rv, client_fd, buf);
if( write(client_fd, buf, rv) < 0)
{
printf("写 %d 个字节的数据给客户端失败: %s\n", rv, client_fd, strerror(errno));
close(client_fd);
}
sleep(1);
close(client_fd);
}
close(listen_fd);
}
客户端:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SEVER_PORT 8889
#define MSG_STR "hello Simon"
int main(int argc, int **argv)
{
int conn_fd = -1;
int rv = -1;
struct sockaddr_in sever_addr;
char buf[1024];
conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if(conn_fd < 0)
{
printf("创建socket失败 :%s \n", strerror(errno));
return -1;
}
memset(&sever_addr, 0, sizeof(sever_addr));
sever_addr.sin_family = AF_INET;
sever_addr.sin_port = htons(SEVER_PORT);
inet_aton(SERVER_IP, &sever_addr.sin_addr);
if( connect(conn_fd, (struct sockaddr *)&sever_addr, sizeof(sever_addr)) )
{
printf("连接服务器[%s:%d]失败:%s\n", SERVER_IP, SEVER_PORT, strerror(errno));
return 0;
}
if(write(conn_fd, MSG_STR, sizeof(MSG_STR)) <0 )
{
printf("给服务器[%s:%d] 写数据失败 :%s ", SERVER_IP, SEVER_PORT, strerror(errno));
goto cleanup;
}
memset(buf, 0, sizeof(buf));
rv = read(conn_fd, buf, sizeof(buf));
printf("%d",rv);
if( rv < 0)
{
printf("从服务器 [%s:%d]读数据失败:%s \n", SERVER_IP, SEVER_PORT, strerror(errno));
goto cleanup;
}
if(rv == 0)
{
printf("客户端连接服务器失败\n");
goto cleanup;
}
printf("从服务器读到 %d 个字节的数据: '%s'\n", rv, buf);
cleanup:
close(conn_fd);
}
参考文献:
《unix 环境高级编程》