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 流模式:
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;
unsigned short int sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
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);
通过流程图,我们可以分别写出服务器和客户端的代码实现步骤。
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;
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);
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));
iRet = listen(iSocketServer, BACKLOG);
while (1)
{
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];
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);
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));
iRet = listen(iSocketServer, BACKLOG);
while (1)
{
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];
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);
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr_in));