【TCP/IP网络编程】:04基于TCP的服务器端/客户端
结合前面所讲述的知识,本篇文章主要介绍了简单服务器端和客户端实现的框架流程及相关函数接口。
理解TCP和UDP
根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字(本系列文章主要围绕TCP的内容讲解)。
TCP(Transmission Control Protocol)即传输控制协议,意为“对数据传输过程的控制”。因此,关注控制方法及范围有助于正确理解TCP套接字。
TCP/IP协议栈
TCP/IP协议栈共分为4层,可以理解为将数据收发分为了4个层次化的过程,如下图所示。各层可以通过操作系统等软件实现,也可通过类似NIC的硬件设备实现。相较于数据通信过程的7层协议栈(OSI 7层模型),对于普通程序员来说掌握这四层就可以了。
TCP/IP协议栈
TCP/IP协议的诞生背景
“通过因特网完成有效的数据传输”这一课题是涉及到了硬件、系统、路由算法等各个领域的一个大系统。因此,当时相关领域的专家就聚在一起讨论,确定将这一大课题按不同领域分成若干小模块,这就出现了多种协议,它们通过层级结构建立了紧密联系。
将协议分为多个层次有很多优点,最重要的原因是为了通过标准化操作设计开放式系统。标准本身就在于对外公开,引导更多人遵守规范。其中,以多个标准为依据设计的系统称为开放式系统,TCP/IP协议栈便是其中之一。
链路层
链路层是物理链接领域标准化的结果,也是最基本的领域,专门定义LAN、WAN、MAN等网络标准。若两台主机通过网络进行数据交换,则需要通过下图所示的物理连接,链路层就负责这些标准。
网络连接结构
IP层
准备好物理连接后就需要传输数据,而在复杂的网络中传输数据,首先就是要考虑通过哪条路径将数据传输至目标主机?这就是IP层协议解决的问题。
IP本身是面向消息、不可靠的协议,因此,IP协议无法应对各种可能的数据错误。
TCP/UDP层
TCP和UDP层以IP层提供的路径信息为基础完成实际的数据传输,故该层又称为传输层。IP层只关注1个数据包(数据传输的基本单位)的传输过程,对于多个数据包的传输也是由IP层完成对每个数据包的实际传输。因此,正如前面所述,IP层对数据传输的过程并不可靠。而TCP协议的性质则向不可靠的IP协议赋予了可靠性,下图是TCP对网络丢包的处理。
TCP协议
应用层
以上协议的处理过程都是套接字通信自动处理的,如选择数据传输路径、数据确认过程,这些都被隐藏到了套接字内部。编写软件的过程中,需要根据程序特点决定服务器端和客户端之间的数据传输规则,这便是应用层协议。而网络编程的大部分内容就是设计并实现应用层协议。
实现基于TCP的服务器端/客户端
TCP服务器端的默认函数调用
大部分服务器端默认函数调用都是按照下图所示的顺序来执行的,其中socket及bind函数前文已有介绍,下面介绍之后的实现过程。
TCP服务器端函数调用顺序
进入等待连接请求状态 - listen
调用listen函数使服务器端进入等待连接请求的状态,此时客户端才能调用connect函数进入发出连接请求的状态,若提前调用connect则会报错(Connection refused)。
#include <sys/socket.h> int listen(int sock, int backlog);
-> 成功时返回0,失败时返回-
其中,backlog为连接请求等待队列的长度,若为N则表示最多使N个连接请求进入队列(连接请求等待队列又分为已连接和未连接等待队列,这里backlog表示已连接等待队列长度,其实目前并未有对backlog参数的确切定义,需要根据实际环境确定)。“服务器端处于等待连接请求状态”是指,客户端连接请求时,受理连接前一直使连接请求处于等待状态,该过程如下图所示。
等待连接请求状态
listen函数的第一个参数是服务器端套接字,如同一个门卫监听到来的连接请求,并将这些请求送往连接请求等候室;第二个参数与服务器的特性有关,根据服务器的工作性质来决定适当的队列大小值。
受理客户端连接请求 - accept
调用listen函数后,若有连接请求则按序受理,受理请求则意味着进入可收发数据的状态。监听套接字已有自己的工作职责,此时需要创建一个新的会话套接字来服务发起连接的客户端套接字。
#include <sys/socket.h> int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
-> 成功时返回创建的套接字文件描述符,失败时返回-
第一个参数是服务器套接字文件描述符;第二个参数用于保存客户端地址信息;第三个参数用于保存客户端地址信息的长度,但首先需要传入地址信息结构长度信息。
accept函数受理连接请求等待队列中待处理的客户端连接请求,成功时返回新生成的用于数据I/O套接字的文件描述符。该I/O套接字是自动创建的,且已自动建立了与发起连接请求的客户端之间的连接。accept函数的调用过程如下图所示。
受理连接请求状态
调用accept函数会从等待连接请求队列头处取1个连接请求与客户端建立连接,并返回创建的套接字文件描述符。如果此时等待队列为空,则accept函数会发生阻塞,直到队列中出现新的客户端连接。
TCP客户端的默认函数调用顺序
客户端的函数调用相较于服务器端要简单许多,因为套接字创建和连接请求便是一个简单客户端的全部内容,其函数调用过程如下。
TCP客户端函数调用顺序
服务器端调用listen函数创建连接请求队列,之后客户端即可发起请求连接。
#include <sys/socket.h> int connect(int sock, struct sockaddr *servaddr, socklen_t addrlen);
-> 成功时返回0,失败时返回-
客户端调用connect函数后,发生以下情况之一时才会返回:
a. 服务器端接收连接请求
b. 发生断网等异常情况而中断连接请求
所谓“接收连接”并不意味着服务器端需要调用accept函数,其实是服务器端把连接请求信息记录到等待队列的过程。因此,connect函数成功返回并不意味着可以立即进行数据交换。
之前的文章中有提到过这样一个疑问,服务器端需要调用bind函数绑定地址信息到服务器端套接字,那为何客户端没有这一过程呢?其实客户端套接字也是需要分配IP和端口号等地址信息的,只不过这一步骤被操作系统隐藏了。那客户端又是何时、何地、如何分配地址呢?
何时? 调用connect函数时
何地? 操作系统,准确说时在内核中
如何? IP使用计算机的IP,端口号随机
基于TCP的服务器端/客户端函数调用关系
服务器端与客户端的函数调用关系并非相互独立的,其交互关系大致如下。其中,需要重点理解客户端connect函数的调用时机及服务器端对connect函数发起连接请求的反馈动作(大名鼎鼎的三次握手就这这个过程中完成)。
函数调用关系
实现迭代服务器端/客户端
以上介绍了TCP的相关知识,下面给出回声服务器端/客户端相关源码以供浏览学习。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 1024
void error_handling(char *message); int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i; struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz; if(argc!=) {
printf("Usage : %s <port>\n", argv[]);
exit();
} serv_sock=socket(PF_INET, SOCK_STREAM, );
if(serv_sock==-)
error_handling("socket() error"); memset(&serv_adr, , sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[])); if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-)
error_handling("bind() error"); if(listen(serv_sock, )==-)
error_handling("listen() error"); clnt_adr_sz=sizeof(clnt_adr); for(i=; i<; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-)
error_handling("accept() error");
else
printf("Connected client %d \n", i+); while((str_len=read(clnt_sock, message, BUF_SIZE))!=)
write(clnt_sock, message, str_len); close(clnt_sock);
} close(serv_sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}
echo_server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h> #define BUF_SIZE 1024
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr; if(argc!=) {
printf("Usage : %s <IP> <port>\n", argv[]);
exit();
} sock=socket(PF_INET, SOCK_STREAM, );
if(sock==-)
error_handling("socket() error"); memset(&serv_adr, , sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[]);
serv_adr.sin_port=htons(atoi(argv[])); if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-)
error_handling("connect() error!");
else
puts("Connected..........."); while()
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin); if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break; write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-);
message[str_len]=;
printf("Message from server: %s", message);
} close(sock);
return ;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}
echo_client
服务器端通过如下实现方式可循环服务发起连接的客户端,但每次仅能服务一个客户端(后续使用多线程或多进程的框架实现便可处理并发的情况)。客户端通过调用close函数主动发起断连请求,服务器端收到该消息(EOF)便从阻塞的read函数中返回,此时read返回值为0。
迭代服务器端代码实现流程
回声客户端存在的问题
回声客户端传输接收数据的流程如下。回顾之前关于TCP性质的介绍,我们知道TCP是没有数据边界的,即write函数传输的数据可能在多次调用之后一次发送;同样read函数的调用也可能在尚未收到全部数据包时返回。那么这个问题该如何解决?
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-);
message[str_len]=;
printf("Message from server: %s", message);
结合服务器端的代码来看,很容易可以知道客户端需要接收数据的大小,因此加一个循环判断read结束条件即可。
recv_len=;
str_len=write(sock, message, strlen(message)); while(recv_len<str_len)
{
recv_cnt=read(sock, &message[recv_len], BUF_SIZE-);
if(recv_cnt==-)
error_handling("read() error!");
recv_len+=recv_cnt;
} message[recv_len]=;
printf("Message from server: %s", message);
上面的实现确实解决了当前所面临的问题,但更多的时候接收数据端并不能确定待接收数据的大小等相关信息。因此,问题的根因并不在于客户端,而是我们应该定义符合需求的应用层协议。比如上面的问题,如果数据收发双方预先协定好数据的边界规则,或将数据包大小等相关信息写入特定字段来表示问题便可得到解决。
【TCP/IP网络编程】:04基于TCP的服务器端/客户端的更多相关文章
- TCP/IP网络编程之基于TCP的服务端/客户端(二)
回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...
- TCP/IP网络编程之基于TCP的服务端/客户端(一)
理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流(stream)的套接字.TCP是Transmissi ...
- TCP/IP网络编程之基于UDP的服务端/客户端
理解UDP 在之前学习TCP的过程中,我们还了解了TCP/IP协议栈.在四层TCP/IP模型中,传输层分为TCP和UDP这两种.数据交换过程可以分为通过TCP套接字完成的TCP方式和通过UDP套接字完 ...
- TCP/IP网络编程之套接字的多种可选项
套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...
- 【TCP/IP网络编程】:09套接字的多种可选项
本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...
- 《TCP/IP网络编程》
<TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...
- TCP/IP网络编程系列之四(初级)
TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...
- TCP/IP网络编程系列之二(初级)
套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,doma ...
- TCP/IP网络编程之优于select的epoll(二)
基于epoll的回声服务端 在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例. echo_epollserv ...
- TCP/IP网络编程之进程间通信
进程间通信基本概念 进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间.但我们知道,进程具有完全独立的内存结构,就连通过fork函数创建的子进程 ...
随机推荐
- 关键路径法(Critical Path Method, CPM)
1.活动节点描述及计算公式 通过分析项目过程中哪个活动序列进度安排的总时差最少来预测项目工期的网络分析. 产生目的:为了解决,在庞大而复杂的项目中,如何合理而有效地组织人力.物力和财力,使之在有限资源 ...
- centos 生成网卡UUID
在Linux或CentOS中,可以通过如下命令获取网卡的uuid信息: uuidgen 网卡名07d07031-eb0f-4691-8606-befb46645433 查看网卡UUID nmcli c ...
- Ubuntu改坏sudoers后无法使用sudo的解决办法
练习安装odoo的时候,创建了一个odoo用户,想把它赋予sudo权限,然而,编辑的时候不留意,改坏了,导致sudo无法使用,无法编辑sudoers文件修改回来. 总提示如下信息: >>& ...
- 扛把子组final week 1/1 Scrum立会报告+燃尽图 01
此作业的要求参见http://edu.cnblogs.com/campus/nenu/2019fall/homework/10065 一.小组情况 队名:扛把子 组长:孙晓宇 组员:宋晓丽 梁梦瑶 韩 ...
- Head First设计模式——适配器和外观模式
前言:为什么要一次讲解这两个模式,说点骚话:因为比较简单(*^_^*),其实是他们两个有相似和有时候我们容易搞混概念. 讲到这两个设计模式与另外一个“装饰者模式”也有相似,他们三个按照结构模式分类都属 ...
- python3 之 闭包实例解析
一.实例1: def make_power(y): def fn(x): return x**y return fn pow3 = make_power(3) pow2 = make_power(2) ...
- JavaScript笔记四
1.运算符 逻辑运算符 ! - 非运算可以对一个布尔值进行取反,true变false false边true - 当对非布尔值使用!时,会先将其转换为布尔值然后再取反 - 我们可以利用!来将其他的数据类 ...
- 请停止编写这么多的for循环!
在这篇文章中,我想和你一起回到基础知识,并讨论 Java 中的 for 循环.老实说,我正在为自己写这篇博客文章,因为我也会这样做.从 Java 8 开始,我们不必在 Java 中编写太多 for 循 ...
- vim介绍、颜色显示和移动光标、一般模式下移动光标及复制、剪切和粘贴
第4周第4次课(4月12日) 课程内容: 5.1 vim介绍5.2 vim颜色显示和移动光标5.3 vim一般模式下移动光标5.4 vim一般模式下复制.剪切和粘贴 5.1 vim介绍 centos7 ...
- Singletone 析构函数调不到
<设计模式>定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例. 关键字:指向自己的静态指针私有,创建对象并赋值私有静态指针函数->公有, 构 ...