Socket是网络协议上的一层抽象接口。本文整理了使用socket实现客户端和服务端的流程。
客户端
1. 使用 socket() 创建TCP套接字
该函数在头文件 sys/socket.h 中:
1 2 3 |
#include <sys/socket.h> int socket(int domain, int type, int protocol); |
socket函数返回一个文件描述符,代表一个套接字,它是一个整数。按照unix惯例,非负整数表示成功。
第一个参数确定套接字的通信领域,常用的有 AF_INET 表示ipv4,AF_INET6 表示ipv6
第二个参数指定套接字的类型,常用的有 SOCK_STREAM ,它表示可靠的字节流。
第三个参数指定端到端的协议,目前只支持一种协议,也就是TCP/IP协议,这里指定0
因此,可以这样使用:
1 2 3 4 |
//创建一个socket int socket_fd = socket(AF_INET, SOCK_STREAM, 0); if (socket_fd < 0) error("socket() failed"); |
2. 使用 connect() 连接服务器
首先,使用套接字需要知道通信的端点的地址,包括ip地址和端口。socket api提供了数据结构用来表示地址信息:
1 2 3 4 |
struct sockaddr { sa_family_t sa_family; char sa_data[14]; } |
这个结构体的第一个成员指定地址族,常用的有 AF_INET 表示ipv4,AF_INET6 表示ipv6 。sockaddr是通用的,而不同的ip版本对应不同的结构
当sa_family指定为 AF_INET 时,就要使用 sockaddr_in 结构
1 2 3 4 5 6 7 8 9 10 |
// IPv4 AF_INET sockets: struct sockaddr_in { short sin_family; // 2 bytes e.g. AF_INET, AF_INET6 unsigned short sin_port; // 2 bytes e.g. htons(3490) struct in_addr sin_addr; // 4 bytes see struct in_addr, below char sin_zero[8]; // 8 bytes zero this if you want to }; struct in_addr { unsigned long s_addr; // 4 bytes load with inet_pton() }; |
我们的例子中 服务器地址是 192.168.123.163,端口是8000 ,可以按照下面这样设置sockaddr_in结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//设置要连接的服务端地址,使用结构体addr表示 struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); //设置ipv4协议族 addr.sin_family = AF_INET; //设置ip地址 int ret = inet_pton(AF_INET, "192.168.123.163", &addr.sin_addr.s_addr); if (ret == 0) error("invalid ip address"); else if (ret < 0) error("inet_pton() failed"); //设置端口号 addr.sin_port = htons(8000); |
设置 sin_family 为 AF_INET ,表示使用ipv4版本
使用 inet_pton 来设置 ip 地址,该函数用于把 点分十进制表示的ip地址转变为 32位的二进制表示
设置端口号8000, 函数 htons 用于 将整型变量转变成网络字节顺序
最后,使用connect()连接服务器
1 2 3 |
//连接服务端 if (connect(socket_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) error("connect() failed"); |
connect的第一个参数是套接字描述符,第二个参数就是上面的表示地址的结构体,第三个参数是表示地址结构体的大小。
由于ipv4和ipv6对应了不同的地址结构体,这里传入指针时强制转换为通用的sockaddr指针,根据第一个成员就可以区分ipv4或者是ipv6,从而可以强制转换回去。
3. 使用 send() 和 recv() 通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//发送字符串hello world char send_buffer[512] = "hello world"; int len = strlen(send_buffer); int n = send(socket_fd, send_buffer, len, 0); if (n < 0) error("send() failed"); else if (n != len) error("send number of bytes error"); printf("send: %s \n", send_buffer); //接受服务器返回 char buffer[512]; n = recv(socket_fd, buffer, 511, 0); if (n < 0) error("recv() failed"); buffer[n] = '\0'; printf("receive: %s \n", buffer); |
send和recv函数的参数非常类似:
1 2 |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
对于发送来说,buf指向要发送的数据,len说明要发送的字节数。
对于接收来说,buf指向接收缓冲区,len说明接收区的最大字节数。
如果没有收到消息,recv() 会阻塞。
需要注意的是,recv一次收到的数据不一定是send一次发送的数据,可能需要多次接收。上面的例子中并没有考虑。
4. 使用close()关闭socket
1 2 |
//关闭socket close(socket_fd); |
服务端
1. 使用 socket() 创建TCP套接字
和客户端是类似的
1 2 3 4 |
//创建一个socket int socket_fd = socket(AF_INET, SOCK_STREAM, 0); if (socket_fd < 0) error("socket() failed"); |
2. 使用 bind() 给套接字分配端口号
首先也是需要一个表示地址的结构体:
1 2 3 4 5 6 7 8 9 |
//设置服务端地址,使用结构体server_addr表示 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); //设置ipv4协议族 server_addr.sin_family = AF_INET; //设置ip地址 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置端口号 server_addr.sin_port = htons(8000); |
和客户端不同的地方是ip地址,设置为服务端可获得的任何ip地址。htonl 用于 将主机数(32位)转换成无符号长整型的网络字节顺序
接下来使用bind函数,将套接字和本地的一个端口相关联。
1 2 3 |
//绑定端口 if (bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) error("bind() error"); |
3. 使用 listen() 监听该端口,允许对该端口建立连接
1 2 3 |
//监听端口 if (listen(socket_fd, 5) < 0) error("listen() error"); |
listen允许客户端连接进入。如果没有执行listen,客户端的connect函数将会失败。
4.1 反复循环,每次使用 accept() 接受连接
需要写到一个循环里,以便反复多次的接收客户端的连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
for (;;) { struct sockaddr_in client_addr; //用来保存客户端地址 int client_addr_len = sizeof(client_addr); int client_socket_fd = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_len); if (client_socket_fd < 0) error("accept() error"); //显示客户端地址 char client_buffer[512]; if (inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_buffer, sizeof(client_buffer)) != NULL) { printf("accept client : %s:%d\n", client_buffer, ntohs(client_addr.sin_port)); } else error("client address error"); //应答客户端 HandleTCPClient(client_socket_fd); } |
accept会阻塞直到有客户端连接进来,它返回的是连接到远程连接的套接字。会把客户端的地址放入第二个参数指向的结构,第三个参数存放参数的大小。
使用函数 inet_ntop() 来把 网络字节序的32位二进制ip地址转换为点分十进制的字符串,它正好和函数 inet_pton() 作用相反
4.2 使用 send() 和 recv() 通信 ,使用 close() 关闭
这里的功能是原封不动地将收到的信息发送回去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void HandleTCPClient(int socket_fd) { //接收客户端消息 char buffer[512]; int n = recv(socket_fd, buffer, 511, 0); if (n < 0) error("recv() failed"); buffer[n] = '\0'; printf("receive: %s \n", buffer); //发送字符串hello world int number_byte_send = send(socket_fd, buffer, n, 0); if (number_byte_send < 0) error("send() failed"); else if (number_byte_send != n) error("send number of bytes error"); printf("send: %s \n", buffer); //关闭socket close(socket_fd); } |
毫无疑问,这个是要支持的!