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

目录
相关文章
|
1月前
|
网络协议 安全 Linux
Linux C/C++之IO多路复用(select)
这篇文章主要介绍了TCP的三次握手和四次挥手过程,TCP与UDP的区别,以及如何使用select函数实现IO多路复用,包括服务器监听多个客户端连接和简单聊天室场景的应用示例。
89 0
|
1月前
|
存储 Linux C语言
Linux C/C++之IO多路复用(aio)
这篇文章介绍了Linux中IO多路复用技术epoll和异步IO技术aio的区别、执行过程、编程模型以及具体的编程实现方式。
73 1
Linux C/C++之IO多路复用(aio)
|
1月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
227 64
|
25天前
|
Unix Linux
Linux | Rsync 命令:16 个实际示例(下)
Linux | Rsync 命令:16 个实际示例(下)
32 3
Linux | Rsync 命令:16 个实际示例(下)
|
18天前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
30天前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
84 5
|
23天前
|
存储 前端开发 测试技术
Android kotlin MVVM 架构简单示例入门
Android kotlin MVVM 架构简单示例入门
28 1
|
1月前
|
安全 Linux Shell
Linux | Rsync 命令:16 个实际示例(上)
Linux | Rsync 命令:16 个实际示例(上)
57 0
Linux | Rsync 命令:16 个实际示例(上)
|
1月前
|
Ubuntu Linux 编译器
Linux/Ubuntu下使用VS Code配置C/C++项目环境调用OpenCV
通过以上步骤,您已经成功在Ubuntu系统下的VS Code中配置了C/C++项目环境,并能够调用OpenCV库进行开发。请确保每一步都按照您的系统实际情况进行适当调整。
261 3
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础