专栏名称: 新机器视觉
最前沿的机器视觉与计算机视觉技术
目录
相关文章推荐
南方人物周刊  ·  中国队逐渐告别“冰强雪弱” ·  13 小时前  
南方人物周刊  ·  200份开工好礼免费送,助你满血复活! ·  22 小时前  
南方人物周刊  ·  冯骥 我们只是一群人在做自己喜欢的事情 | ... ·  昨天  
每日人物  ·  4亿人都在用的必需品,集体“塌房”? ·  3 天前  
51好读  ›  专栏  ›  新机器视觉

一文了解网络通信和网络编程实操

新机器视觉  · 公众号  ·  · 2025-01-18 21:31

正文

一、网络通信概述


1. IP 和端口


所有的数据传输,都有三个要素:源、目的、长度。 怎么表示源跟目的呢?如下图:



所以,在网络传输中需要使用“IP和端口”来表示源或目的。


2. 网络传输中的两个对象:server 和 client


我们经常访问网站,这涉及 2 个对象:网站服务器,浏览器。网站服务器平时安静地呆着,浏览器主动发起数据请求。网站服务器、浏览器可以抽象成 2 个软件的概念: server 程序、client 程序。



3. 网络协议层


在一般的网络书籍中,网络协议被分为5层,如图:



3.1 应用层


应用层是体系中的最高层,直接为用户的应用进程(如电子邮件、文件传输等)提供服务。在因特网中的应用层协议很多,如支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议,支持文件传送的 FTP 协议,DNS, POP3, SNMP, Telnet等。


3.2 运输层


运输层是负责向两个主机中进程之间的通信提供服务。


主要使用以下两种协议:


(1)传输控制协议 TCP(Transmission Control Protocol)


面向连接的,数据传输的单位是报文段,能够提供可靠的交付。


(2)用户数据包协议 UDP(User Datagram Protocol)


无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力的交付”。


3.3 网络层


负责将被成为数据包(datagram)的网络层分组从一台主机移动到另一台主机。


3.4 链路层


因特网的网络层通过源和目的地之间的一系列路由器路由数据报。


3.5 物理层


在物理层上面所传数据的单位是比特,物理层的任务是透明地传送比特流。


这些层对于初学者来说很难理解,我们只需要知道:


我们需要用“运输层”编写应用程序,我们的应用程序位于“应用层”。


使用“运输层”时,可以选择 TCP 协议,也可以选择 UDP 协议。


4. 两种传输方式:TCP/UDP


4.1 TCP 和 UDP 原理上的区别


TCP 向它的应用程序提供了面向连接的服务。这种服务有2个特点:可靠传输、流量控制(即发送方/接收方速率匹配)。它包括了应用层报文划分为短报文,并提供拥塞控制机制。


UDP 协议向它的应用程序提供无连接服务。它没有可靠性,没有流量控制,也没有拥塞控制。


既然 TCP 提供了可靠数据传输服务,而 UDP 不能提供,那么 TCP 是否总是首选呢? 答案是否定的,因为有许多应用更适合用 UDP,举个例子:视频通话时,使用 UDP,偶尔的丢包、偶尔的花屏时可以忍受的;如果使用 TCP,每个数据包都要确保可靠传输,当它出错时就重传,这会导致后续的数据包被阻滞,视频效果反而不好。


关于何时发送什么数据控制的更为精细


采用 UDP 时只要应用进程将数据传递给 UDP,UDP 就会立即将其传递给网络层。而 TCP 有重传机制,并且不管可靠交付需要多长时间。但是实时应用通常不希望过分的延迟报文段的传送,可以能容忍一部分数据丢失。


TCP 特点:

基于流的方式;

面向连接;

可靠通信方式;


UDP 特点:

无需建立连接,不会引入建立连接时的延迟;

无连接状态,能支持更多的活跃客户;

分组首部开销较小;


4.2 TCP/UDP 网络通信大概交互图


面向连接的 TCP 流模式:



UDP 用户数据包模式:




二、网络编程主要函数介绍


1. socket 函数


此函数用于创建一个套接字,它的函数原型如下:


int socket(int domain, int type,int protocol); 


domain 是网络程序所在的主机采用的通讯协族(AF_UNIX 和 AF_INET 等)。


AF_UNIX 只能够用于单一的 Unix 系统进程间通信,而 AF_INET 是针对 Internet 的,因而可以允许远程通信使用。


type 是网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM 等)。


SOCK_STREAM 表明用的是 TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流。


SOCK_DGRAM 表明用的是 UDP 协议,这样会提供不可靠,无连接的通信。


关于 protocol,由于指定了 type,所以这个地方一般只要用0来代替就可以了。


此函数执行成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况。


2. bind 函数


此函数用于将地址绑定到一个套接字,它的函数原型如下:


int bind(int sockfd, const struct sockaddr *addr, int addrlen);


sockfd 是由 socket 函数调用返回的文件描述符。

my_addr 是一个指向 sockaddr 的指针。

addrlen 是 sockaddr 结构的长度。


sockaddr 的定义:


struct sockaddr {     unisgned short  as_family;     char sa_data[14]; }; 


不过由于系统的兼容性,我们现在都使用另外一个结构(struct sockaddr_in) 来代替。 sockaddr_in 的定义:


struct sockaddr_in {    short int sin_family;      // 地址族,一般设为 AF_INET 表示IPv4    unsigned short int sin_port;  // 端口号,使用网络字节序    struct in_addr sin_addr;    // IPv4地址结构体,同样用网络字节序保存IP地址    char sin_zero[8];           // 填充字段,用于将结构体大小凑齐到和 sockaddr 结构体一样大小等用途};


sin_family 使用 Internet 一般设置为 AF_INET 即可。


sin_addr 设置为INADDR_ANY表示可以和任何的主机通信。


sin_port 是要监听的端口号。


sin_zero 主要是用于填充字节,确保 struct sockaddr_in 结构体的大小和 struct sockaddr 结构体一样,并且其内容通常不影响前面已经设置好的关键信息的功能。在设置完关键信息后清零 sin_zero 可以保证结构体初始化的完整性和正确性,为后续的网络操作(如bind函数调用等)提供正确的参数。


bind 将本地的端口同 socket 返回的文件描述符捆绑在一起,成功是返回0,失败的情况和 socket 一样为 -1。


简单来说 bind 函数就是让套接字文件在通信时使用固定的IP和端口号(针对服务器来说),调用socket函数创建的套接字仅仅执行了通信等协议,但是并没有指定通信时所需的ip地址和端口号。


ip 是对方设备的唯一标识

端口号区分同一台计算机上的不同的网络通信进程


如果不调用 bind 函数指定 ip 和端口,则会自己指定一个 ip 和端口,此时违背了 TCP 通信的可靠性和面向连接的特点。


服务器如何知道客户端的ip和端口号


可以通过上文TCP通信模型中看到,客户端通信时不需要指定ip和端口号,直接创建一个socket套接字文件描述符即可参与通信。 此时当客户端和服务器建立连接的时候,服务器会从客户的数据包中提取出客户端ip和端口,并保存起来,如果是跨网通信,那么记录的就是客户端所在路由器的公网ip。


3. listen 函数


此函数为服务器监听函数,宣告服务器可以接受连接请求,它的函数原型如下:


int listen(int sockfd, int backlog);


sockfd 是bind后的文件描述符。


backlog 设置请求排队的最大长度。当有多个客户端程序和服务端相连时,使用这个表示可以介绍的排队长度。


listen 函数将 bind 的文件描述符变为监听套接字,返回的情况和bind一样。


4. accept 函数


此函数用于服务器获得连接请求,并且建立连接,它的函数原型如下:


int accept(int sockfd, struct sockaddr *addr,int *addrlen);


sockfd 是listen 后的文件描述符。


addr,addrlen 用来存放客户端的信息。


listen监听客户端来的链接,accept将客户端的信息绑定到一个socket上,也就是给客户端创建一个socket,通过返回值返回给我们客户端的socket。


accept 调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接。 accept 成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了,失败时返回 -1 。


5. connect 函数


此函数用来建立一个连接,它的函数原型如下:


int connect(int sockfd, struct sockaddr * serv_addr,int addrlen); 


sockfd 是socket 函数返回的文件描述符。


serv_addr 储存了服务器端的连接信息,其中 sin_add 是服务端的地址。


addrlen 是 serv_addr 的长度。


connect 函数是客户端用来同服务端连接的,成功时返回0,


6. send 函数


此函数用来发送数据,它的函数原型如下:


ssize_t send(int sockfd, const void *buf, size_t len, int flags);


sockfd:指定发送端套接字描述符


服务器:服务器与特定客户端连接的套接字

客户端:客户端与服务器建立连接的套接字


buf 指明一个存放应用程序要发送数据的缓冲区;


len 指明实际要发送的数据的字节数;


flags 一般置0。


客户端或者服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数据。


7. recv 函数


此函数用来接收数据,它的函数原型如下:


ssize_t recv(int sockfd, void *buf, size_t len, int flags);


sockfd 指定接收端套接字描述符;


buf 指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;


len 指明buf的长度;


flags 一般置0。


客户或者服务器应用程序都用 recv 函数从 TCP 连接的另一端接收数据。


8. 其它常见转换函数


8.1 htons 函数


uint16_t htons(uint16_t hostshort);


htons的功能:将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)。


第一个问题:为什么使用两个字节,也就是16位来存储。

这个简单一些,因为一个字节只能存储8位2进制数,而计算机的端口数量是65536个,也就是2^16,两个字节。


第二个为题:为什么计算机需要大端模式和小端模式?


小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。


大端模式 :符号位的判定固定为第一个字节,容易判断正负。


8.2 inet_aton 函数


此函数用于将 IP 地址字符串转换为网络字节序的二进制 IP 地址结构的函数,它的函数原型如下:


int inet_aton(const char *cp, struct in_addr *inp);


cp是一个指向以点分十进制表示的 IP 地址字符串的指针(例如 "192.168.1.1")。


inp是一个指向 struct in_addr 类型的指针,用于存储转换后的 IP 地址。


如果转换成功,函数返回非零值;如果转换失败,返回 0。


8.3 inet_ntoa 函数


此函数用于将网络字节序的二进制 IP 地址转换为字符串 IP 地址。


char *inet_ntoa(struct in_addr in);



三、TCP编程示例



通过流程图,我们可以分别写出服务器和客户端的代码实现步骤。


1. 服务器与客户端连接


1.1 服务器


我们先实现服务器与客户端连接的代码,再实现服务器与客户端收发数据。 主要实现步骤如下:


1)socket 创建套接字

2)bind 绑定套接字

3)listen 监听客户端连接

4)accept 连接客户端


服务器等待客户端连接代码示例:


#include       /* See NOTES */#include #include #include #include #include 
#define SERVER_PORT 8888#define BACKLOG     10
int main(int argc, char **argv){  int iSocketServer;  int iSocketClient;  struct sockaddr_in tSocketServerAddr;  struct sockaddr_in tSocketClientAddr;
 int iRet;  int iAddrlen;  int iClientNum = 0;    /* 1. socket */  iSocketServer = socket(AF_INET, SOCK_STREAM, 0);    tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;  tSocketServerAddr.sin_family      = AF_INET;  tSocketServerAddr.sin_port      = htons(SERVER_PORT);  memset(tSocketServerAddr.sin_zero, 0, 8);
 /* 2. bind   */  iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));
 /* 3. listen */  iRet = listen(iSocketServer, BACKLOG);
 while (1)  {      /* 4. accept */    iAddrlen = sizeof(struct sockaddr_in);    iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrlen);    if (iSocketClient != -1)    {      iClientNum++;      printf("get connect from client %d:%s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));    }  }
 close(iSocketServer);    return 0;}


1.2 客户端


在这里同样我们先实现客户端连接服务器的代码,再实现两者收发数据。 主要实现步骤如下:


1)socket 创建套接字

2)connect 连接服务器


客户端连接服务器代码示例:


#include




    
       /* See NOTES */#include #include #include #include #include 
#define SERVER_PORT 8888#define BACKLOG     10
int main(int argc, char **argv){  int iSocketServer;  int iSocketClient;  struct sockaddr_in tSocketServerAddr;  struct sockaddr_in tSocketClientAddr;
 int iRet;  int iAddrlen;  int iClientNum = 0;  int cnt = 0;
 int iRcvLen;  int iSendLen;  unsigned char ucSendBuf[1000];  unsigned char ucRcvBuf[1000];    /* 1. socket */  iSocketServer = socket(AF_INET, SOCK_STREAM, 0);    tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;  tSocketServerAddr.sin_family      = AF_INET;  tSocketServerAddr.sin_port      = htons(SERVER_PORT);  memset(tSocketServerAddr.sin_zero, 0, 8);
 /* 2. bind   */  iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));
 /* 3. listen */  iRet = listen(iSocketServer, BACKLOG);
 while (1)  {      /* 4. accept */    iAddrlen = sizeof(struct sockaddr_in);    iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrlen);    if (iSocketClient != -1)    {      iClientNum++;      printf("get connect from client %d:%s\n", iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
     /* 创建一个子进程 */      if (!fork())      {        while (1)        {          /* 接收服务器发过来的数据 */          iRcvLen = recv(iSocketClient, ucRcvBuf, 999, 0);          if (iRcvLen > 0)          {            ucRcvBuf[iRcvLen] = '\0';            printf("get msg from client:%s\n", ucRcvBuf);
           /* 发送应答数据给客户端 */            sprintf(ucSendBuf, "send ACK %d to client", ++cnt);            iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);          }          else          {            close(iSocketClient);            return -1;          }        }      }    }  }
 close(iSocketServer);    return 0;}



编译运行测试:



在这里,我们已经初步实现了客户端与服务器连接,接着我们实现双方数据交互。


2. 服务器与客户端数据交互


2.1 服务器


我们只需要在原来的代码上实现发送和接收功能:


1)send 发送

2)recv 接收


代码示例:


#include       /* See NOTES */#include #include #include #include #include 
#define SERVER_PORT 8888#define BACKLOG     10
int main(int argc, char **argv){  int iSocketServer;  int iSocketClient;  struct sockaddr_in tSocketServerAddr;  struct sockaddr_in tSocketClientAddr;
 int iRet;  int iAddrlen;  int iClientNum = 0;  int cnt = 0;
 int iRcvLen;  int iSendLen;  unsigned char ucSendBuf[1000];  unsigned char ucRcvBuf[1000];    /* 1. socket */  iSocketServer = socket(AF_INET, SOCK_STREAM, 0);    tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;  tSocketServerAddr.sin_family      = AF_INET;  tSocketServerAddr.sin_port      = htons(SERVER_PORT);  memset(tSocketServerAddr.sin_zero, 0, 8);
 /* 2. bind   */  iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));
 /* 3. listen */






请到「今天看啥」查看全文


推荐文章
南方人物周刊  ·  中国队逐渐告别“冰强雪弱”
13 小时前
南方人物周刊  ·  200份开工好礼免费送,助你满血复活!
22 小时前
水木文摘  ·  被人爱着,真好啊
7 年前
猎奇漫画部  ·  神结局漫画丨我们要进行一个大计划
7 年前
ME锤锤  ·  最近在看什么书?
7 年前