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的客户端和服务器的开发流程及对应实现。

目录
相关文章
|
18天前
|
算法 编译器 C语言
探索C++编程的奥秘与魅力
探索C++编程的奥秘与魅力
|
18天前
|
编译器 C语言 C++
C语言,C++编程软件比较(推荐的编程软件)
C语言,C++编程软件比较(推荐的编程软件)
|
4天前
|
算法 安全 编译器
【C++】从零开始认识泛型编程 — 模版
泛型编程是C++中十分关键的一环,泛型编程是C++编程中的一项强大功能,它通过模板提供了类型无关的代码,使得C++程序可以更加灵活和高效,极大的简便了我们编写代码的工作量。
13 3
|
5天前
|
存储 算法 编译器
C++的模板与泛型编程探秘
C++的模板与泛型编程探秘
9 0
|
5天前
|
Ubuntu Android开发 数据安全/隐私保护
【Android平板编程】远程Ubuntu服务器Code-Server编程写代码
【Android平板编程】远程Ubuntu服务器Code-Server编程写代码
|
6天前
|
JSON Java Linux
【探索Linux】P.30(序列化和反序列化 | JSON序列化库 [ C++ ] )
【探索Linux】P.30(序列化和反序列化 | JSON序列化库 [ C++ ] )
20 2
|
6天前
|
存储 算法 网络协议
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
【探索Linux】P.26(网络编程套接字基本概念—— socket编程接口 | socket编程接口相关函数详细介绍 )
12 0
|
6天前
|
存储 安全 算法
【Linux | C++ 】基于环形队列的多生产者多消费者模型(Linux系统下C++ 代码模拟实现)
【Linux | C++ 】基于环形队列的多生产者多消费者模型(Linux系统下C++ 代码模拟实现)
22 0
|
6天前
|
算法 Linux 数据安全/隐私保护
【Linux | C++ 】生产者消费者模型(Linux系统下C++ 代码模拟实现)
【Linux | C++ 】生产者消费者模型(Linux系统下C++ 代码模拟实现)
12 0
|
14天前
|
编解码 JavaScript 前端开发
【专栏】介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例
【4月更文挑战第29天】本文介绍了字符串Base64编解码的基本原理和在Java、Python、C++、JavaScript及Go等编程语言中的实现示例。Base64编码将24位二进制数据转换为32位可打印字符,用“=”作填充。文中展示了各语言的编码解码代码,帮助开发者理解并应用于实际项目。