结合前面所讲述的知识,本篇文章主要介绍了简单服务器端和客户端实现的框架流程及相关函数接口。

理解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的服务器端/客户端的更多相关文章

  1. TCP/IP网络编程之基于TCP的服务端/客户端(二)

    回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...

  2. TCP/IP网络编程之基于TCP的服务端/客户端(一)

    理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流(stream)的套接字.TCP是Transmissi ...

  3. TCP/IP网络编程之基于UDP的服务端/客户端

    理解UDP 在之前学习TCP的过程中,我们还了解了TCP/IP协议栈.在四层TCP/IP模型中,传输层分为TCP和UDP这两种.数据交换过程可以分为通过TCP套接字完成的TCP方式和通过UDP套接字完 ...

  4. TCP/IP网络编程之套接字的多种可选项

    套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...

  5. 【TCP/IP网络编程】:09套接字的多种可选项

    本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...

  6. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  7. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

  8. TCP/IP网络编程系列之二(初级)

    套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,doma ...

  9. TCP/IP网络编程之优于select的epoll(二)

    基于epoll的回声服务端 在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例. echo_epollserv ...

  10. TCP/IP网络编程之进程间通信

    进程间通信基本概念 进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间.但我们知道,进程具有完全独立的内存结构,就连通过fork函数创建的子进程 ...

随机推荐

  1. Docker+Dubbo+Zookeeper实现RPC远程调用

    Docker+Dubbo+Zookeeper 1.安装Docker 1.1卸载旧版本的Docker //如果Docker处于与运行状态 未运行可跳过 [root@MrADiao ~]# systemc ...

  2. PHP产生不重复随机数的5个方法总结

    无论是Web应用,还是WAP或者移动应用,随机数都有其用武之地.在最近接触的几个小项目中,我也经常需要和随机数或者随机数组打交道,所以,对于PHP如何产生不重复随机数常用的几种方法小结一下 无论是We ...

  3. 理解Redis的反应堆模式

    1. Redis的网络模型 Redis基于Reactor模式(反应堆模式)开发了自己的网络模型,形成了一个完备的基于IO复用的事件驱动服务器,但是不由得浮现几个问题: 为什么要使用Reactor模式呢 ...

  4. vuejs中的回车事件

    @keyup.enter.native="事件名称"

  5. Redis的存储类型、集群架构、以及应用场景

    什么是redis redis是一种支持Key-Value等多种数据结构的存储系统.可用于缓存.事件发布或订阅.高速队列等场景.该数据库使用ANSI C语言编写,支持网络,提供字符串.哈希.列表.队列. ...

  6. python变量、输入输出-xdd

    1.注释 #输入身高,计算BMI 注释1,单行注释... 注释2,多行注释xiedong.. 2.中文编码声明,UTF-8编码声明 # coding=编码 # coding=utf-8 3.建议每行不 ...

  7. 简单入门Kubernetes

    什么是Kubernetes 官网 https://kubernetes.io/ 中文版:https://kubernetes.io/zh/ 个人理解 基于容器技术 分布式架构 弹性伸缩 隔离物理机 和 ...

  8. Prometheus 自动发现

    目录 简介 环境说明 静态配置 重新加载配置文件 基于文件发现配置 重新加载配置文件 添加主机测试 基于DNS的A记录 修改配置文件 重新加载配置文件 基于DNS的SRV记录自动发现 修改配置文件 重 ...

  9. python logging模块小记

    1.简单的将日志打印到屏幕 import logging logging.debug('This is debug message') logging.info('This is info messa ...

  10. react中将svg做成icon组件在其他模块调用

    开发前端页面,经常会有很多共用的图标icon,那么我们把它单独做成组件,以便后期重复调用! 首先在components 的icons文件夹下创建BaseIcon.js文件. 我们需要先在命令行安装gl ...