Android C++ 系列:Linux Socket 编程(三)CS 模型示例

简介: 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于 监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服 务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK 段,服务器收到后从accept()返回。

image.png


1. TCP通信


下图是基于TCP协议的客户端/服务器程序的一般流程:


image.png


服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于 监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服 务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK 段,服务器收到后从accept()返回。


数据传输的过程:


建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是 由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept() 返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处 理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发 回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一 条请求,如此循环下去。


如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样, 服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注 意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方 调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。


在学习socket API时要注意应用程序和TCP协议层是如何交互的:


  • 应用程序调用某个 socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
  • 应用程序如何知 道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段, 再比如read()返回0就表明收到了FIN段


1.1 server


下面通过最简单的客户端/服务器程序的实例来学习socket API。 server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。


/* server.c */
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <arpa/inet.h>
#define MAXLINE 80 
#define SERV_PORT 8000
int main(void) {
  struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len;
  int listenfd, connfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN]; int i, n;
  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr)); 
  servaddr.sin_family = AF_INET; 
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
  servaddr.sin_port = htons(SERV_PORT);
  bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); 
  listen(listenfd, 20);
  printf("Accepting connections ...\n"); 
  while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE); 
    printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    for (i = 0; i < n; i++) 
      buf[i] = toupper(buf[i]);
    write(connfd, buf, n);
    close(connfd); 
  }
}


1.2 client


client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的 字符串并打印。


/* client.c */
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <sys/socket.h> 
#include <netinet/in.h>
#define MAXLINE 80 
#define SERV_PORT 8000
int main(int argc, char *argv[]) {
  struct sockaddr_in servaddr; 
  char buf[MAXLINE];
  int sockfd, n;
  char *str;
  if (argc != 2) {
    fputs("usage: ./client message\n", stderr); exit(1);
  }
  str = argv[1];
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr)); 
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); 
  servaddr.sin_port = htons(SERV_PORT);
  connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  write(sockfd, str, strlen(str));
  n = read(sockfd, buf, MAXLINE); 
  printf("Response from server:\n"); write(STDOUT_FILENO, buf, n);
  close(sockfd);
  return 0; 
}


由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。 客户端和服务器启动后可以查看链接情况:


netstat -apn|grep 8000


2. UDP


image.png


由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很 多保证通讯可靠性的机制需要在应用层实现。


编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并 发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联 系上。和前面TCP程序的运行结果相比较,体会无连接的含义。


2.1 server


/* server.c */
#include <stdio.h>
#include <string.h> 
#include <netinet/in.h> 
#include "wrap.h"
#define MAXLINE 80 
#define SERV_PORT 8000
int main(void) {
  struct sockaddr_in servaddr, cliaddr; 
  socklen_t cliaddr_len;
  int sockfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN]; 
  int i, n;
  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
  bzero(&servaddr, sizeof(servaddr)); 
  servaddr.sin_family = AF_INET; 
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
  servaddr.sin_port = htons(SERV_PORT);
  Bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  printf("Accepting connections ...\n"); 
  while (1) {
    cliaddr_len = sizeof(cliaddr);
    n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len); 
    if (n == -1)
      perr_exit("recvfrom error"); 
    printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
    for (i = 0; i < n; i++) 
      buf[i] = toupper(buf[i]);
    n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); 
    if (n == -1)
      perr_exit("sendto error");
  } 
}


2.2 client


/* client.c */
#include <stdio.h> 
#include <string.h> 
#include <unistd.h> 
#include <netinet/in.h> 
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[]) {
  struct sockaddr_in servaddr; 
  int sockfd, n;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN]; 
  socklen_t servaddr_len;
  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
  bzero(&servaddr, sizeof(servaddr)); 
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); 
  servaddr.sin_port = htons(SERV_PORT);
  while (fgets(buf, MAXLINE, stdin) != NULL) {
    n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); 
    if (n == -1)
      perr_exit("sendto error");
    n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0); 
    if (n == -1)
      perr_exit("recvfrom error");
    Write(STDOUT_FILENO, buf, n); 
  }
  Close(sockfd);
  return 0; 
}


3. 总结


本文分别介绍了基于TCP和UDP的客户端和服务器的开发流程及对应实现。

目录
相关文章
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
572 0
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
992 1
Linux C/C++之IO多路复用(aio)
|
安全 算法 Ubuntu
Linux(openssl)环境:编程控制让证书自签的技巧。
总结:在Linux环境中,OpenSSL是一个非常实用的工具,可以帮助我们轻松地生成自签名证书。通过上述三个简单步骤,即可为内部网络、测试环境或开发环境创建自签名证书。但在公共访问场景下,建议购买经过权威认证机构签发的证书,以避免安全警告。
777 13
|
Linux 数据安全/隐私保护
使用Linux命令行接入无线网络Wi-Fi的示例。
现在,你已经使用命令行成功地连接到 Wi-Fi 网络了。这两个示例涵盖了用 `nmcli` 和 `wpa_supplicant` 连接无线网络的常见场景,让你能够不依赖图形化界面来完成这个任务。在日常使用中熟练掌握这些基本操作能增强你对 Linux 系统的理解,帮助你更有效地处理各种问题。
1254 12
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
374 26
|
JavaScript Ubuntu Linux
如何在阿里云的linux上搭建Node.js编程环境?
本指南介绍如何在阿里云Linux服务器(Ubuntu/CentOS)上搭建Node.js环境,包含两种安装方式:包管理器快速安装和NVM多版本管理。同时覆盖全局npm工具配置、应用部署示例(如Express服务)、PM2持久化运行、阿里云安全组设置及外部访问验证等步骤,助你完成开发与生产环境的搭建。
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
374 17
|
Linux Shell
Linux 10 个“who”命令示例
Linux 10 个“who”命令示例
475 14
Linux 10 个“who”命令示例
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
872 13
|
Unix Linux
Linux | Rsync 命令:16 个实际示例(下)
Linux | Rsync 命令:16 个实际示例(下)
Linux | Rsync 命令:16 个实际示例(下)