134-开始通信

这恐怕是大家最关心的问题了。

在上一文中,接受连接也完成了三次握手的过程,但是我们却不知道如何让两个进程互相发送数据。

实际上我们知道,对于服务器进程来说,三次握手建立完成后的连接,仍然在未决连接队列中,要想让进程通信,服务器进程就需要从未决连接队列中取出连接。此过程由函数 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 函数的语义及其用法
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值