这恐怕是大家最关心的问题了。
在上一文中,接受连接也完成了三次握手的过程,但是我们却不知道如何让两个进程互相发送数据。
实际上我们知道,对于服务器进程来说,三次握手建立完成后的连接,仍然在未决连接队列中,要想让进程通信,服务器进程就需要从未决连接队列中取出连接。此过程由函数 accept 函数完成。
1. accept 函数
(1) 函数原型
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
第一个参数是被动 socket 的描述符。后面两个参数是传出参数,表示对端进程的套接字地址。如果我们对对方的套接字地址不关心,后面两个参数可以传 NULL.
(2) 函数语义
accept 函数从未决连接队列的队头取出一个连接,同时创建一个新的 socket 并返回其描述符。
如果未决连接队列为空,默认情况下(sockfd 是阻塞 IO 描述符) accept 函数会阻塞,直到未决连接队列不空。
通信的关键在于,accept 会创建新的 socket 并返回描述符。接下来,服务器进程利用这个新的 socket 和客户端通信。
图 1 和图 2 演示了接受连接,到 accept 取出连接建立通信链路的过程。
图1 接受连接
图2 建立通信链路
2. 实验
程序 serv.c 在前几篇文章的基础上做了一些修改,主要是添加了 accept 函数,同时向对端发送数据 hello world!\n
。
程序 cli.c 也稍作修改,主要是从套接字读取数据,并打印到屏幕。
程序 cli 与 serv 通信过程如图 3 所示。
图3 cli 与 serv 通信
2.1 serv 程序
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
struct sockaddr_in servaddr, cliaddr;
int sockfd, clientfd, ret;
socklen_t cliaddrlen;
// 1. create sockaddr
puts("1. create sockaddr");
servaddr.sin_family = AF_INET;
// 绑定任意一个网卡上的地址。这意味着,任何一个网卡上有数据都可以接收。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
// 2. create socket
puts("2. create socket");
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) ERR_EXIT("socket");
// 3. bind sockaddr
puts("3. bind sockaddr");
ret = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret < 0) ERR_EXIT("bind");
// 4. listen
puts("4. listen");
ret = listen(sockfd, 5);
if (ret < 0) ERR_EXIT("listen");
// 5. accept connect
puts("5. accept connect");
cliaddrlen = sizeof(cliaddr);
clientfd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (clientfd < 0) ERR_EXIT("accept");
printf("client fd: %d\n", clientfd);
printf("sockaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 6. getsockname, 打印新的 socket 绑定的套接字地址
puts("6. getsockname");
cliaddrlen = sizeof(cliaddr);
ret = getsockname(clientfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (ret < 0) ERR_EXIT("getsockaddr");
printf("sockaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 7. send data
write(clientfd, "Hello wolrd!\n", 13);
close(clientfd);
close(sockfd);
return 0;
}
2.2 cli 程序
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int sockfd, ret, n;
char buf[64];
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
servaddr.sin_family = AF_INET;
// 127.0.0.XXX 表示本机地址。
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(8080);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) ERR_EXIT("socket");
ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret < 0) ERR_EXIT("connect");
cliaddrlen = sizeof(cliaddr);
ret = getsockname(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (ret < 0) ERR_EXIT("getsockaddr");
printf("cliaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 从 socket 读数据
n = read(sockfd, buf, 64);
write(STDOUT_FILENO, buf, n);
close(sockfd);
return 0;
}
2.3 编译和运行
$ gcc serv.c -o serv
$ gcc cli.c -o cli
图4 运行结果
历史上的第一次,出现这个画面,应该是相当激动的,我知道,你也是。
3. 总结
- 掌握 accept 函数的语义及其用法