1 了解数据传输三要素和IP与端口以及两者之间的关系
所有的数据传输,都要三要素:源,目的,长度。
如何表示源与目的呢?
如图:
当同一个笔记本上,同时有两个浏览器访问同一个网站,浏览器发出的源IP相同,服务器IP也一样,目的端口也一样。服务器根据不同源端口区分浏览器。
当同一个笔记本上,有两个不同的软件ssh cilent 和 浏览器要访问同一个网站,ssh要使用ssh服务,而浏览器要使用http服务。两个软件发出的源IP是一样的,目的IP也是一样的。它们如何声明自己要什么服务?通过目的端口来区分。(一般来说,80端口是http服务,22端口是ssh服务)
所以在网络传输中用IP和端口老表示源和目的。
2 网络传输
2.1网络传输的两个对象:server 和 client
我们经常访问网站,这涉及 2 个对象:网站服务器,浏览器。网站服务器平 时安静地呆着,浏览器主动发起数据请求。网站服务器、浏览器可以抽象成 2 个 软件的概念:server 程序、client 程序。
2.2 客户端如何发送数据给服务器?
前言:在一般的网络书籍中,网络协议被分为 5 层
(1)应用层:它是体系结构中的最高层,直接为用户的应用进程(例如电子邮件、 文件传输和终端仿真)提供服务。在因特网中的应用层协议很多,如支持万维网 应用的 HTTP 协议,支持电子邮件的 SMTP 协议,支持文件传送的 FTP 协议,DNS, POP3,SNMP,Telnet 等等。
(2)运输层:负责向两个主机中进程之间的通信提供服务。 运输层主要使用以下两种协议: 传输控制协议 TCP(Transmission Control Protocol):面向连接的,数据传输 的单位是报文段,能够提供可靠的交付。 用户数据包协议 UDP(User Datagram Protocol):无连接的,数据传输的单 位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”。
(3)网络层:负责将被称为数据包(datagram)的网络层分组从一台主机移动到 另一台主机。 (4) 链路层:因特网的网络层通过源和目的地之间的一系列路由器路由数据报。
(5) 物理层:在物理层上所传数据的单位是比特。物理层的任务就是透明地传送 比特流。
网络通信的两个传输方式:TCP/UDP
(1).TCP:可靠传输,流量控制(及双方速率匹配),提供拥塞机制。
(2).UDP:无可靠性,无流量控制,无阻塞机制。
两者分别适用于哪些地方?
(1)因为TCP有堵塞机制,所以TCP适用于命令传输,发送端发送的命令后,等待命令传输到,如果前面的命令传不过去,后面的命令等待前面的命令,所以命令一定不会漏掉。(不容忍数据丢失)
(2)因为TDP没有堵塞机制,所以TDP适用于视频通话时,视频通话发送的每一帧,用户端只管发送,不管是否发送到,发完一帧后,过一段时间后发送后一帧,如果前面的还没有发送过去,则新的帧会覆盖前一帧,我们视频通话的时候,不想一帧发不过去,后面的全部卡住,所以可以使用TDP协议。(容忍数据丢失)
2.21 TCP编程
前言:何为套接字,何为文件描述符
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象(有点像设备节点的作用,我也不清楚)。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
在Linux系统中,套接字是一种特殊类型的文件,它使用整数类型的文件描述符(File Descriptor)来标识。文件描述符是操作系统中用于标识文件的唯一编号,可以用于对文件进行各种读写操作
TCP网络传输交互图
TCP编程相关函数
1.socket
用于创建个套接字(根据指定类型和协议来分配套接口的描述字及其所用到资源的函数)(刚刚看到这个函数的作用,我有点懵,创建一个套接字?返回值都是文件描述符?哪里来的套接字?难道套接字就是文件描述符?我查了查,就是上文的前言:何为套接字,何为文件描述符,我看到了套接字是一种特殊类型的文件,它使用整数类型的文件描述符(File Descriptor)来标识,哦!我才恍然明白,我们获得的文件描述符,就是用来标识我们创建的套接字的)
int socket(int domain , int type , int protocol)
(1).domain 是网络程序所在的主机采用的通讯协族(AF_UNIX 和 AF_INET 等)。 AF_UNIX 只能够用于单一的 Unix 系统进程间通信,而 AF_INET 是针 对 Internet 的,因而可以允许远程通信使用。
(2).type 是网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM 等)。 ◼ SOCK_STREAM 表明用的是 TCP 协议,这样会提供按顺序的,可靠,双 向,面向连接的比特流。 ◼ SOCK_DGRAM 表明用的是 UDP 协议,这样只会提不可靠,无连接的通 信。
(3). protocol,由于指定了 type,所以这个地方一般只要用 0 来代替就可 以了。
(4).返回值,此函数执行成功时返回文件描述符(相当于创建了一个句柄,句柄是文件IO函数的返回值,比如我read一个文件,该read函数的返回值就是句柄,我们怎么知道我读的是谁?通过句柄句柄就是用来标识一个唯一的对象),失败时返回-1,看 errno 可知道出错的详细情况。
2.bind
从函数用于将地址绑定到一个套接字。(其实socket实际上并没有指定IP和端口,实际上指定IP和端口的是bind函数)
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
(1).sockfd 是由 socket 函数调用返回的文件描述符(及上文所述的句柄)。
(2).my_addr 是一个指向 sockaddr (sockaddr与sockaddr_in指针作用相同)的指针。
struct sockaddr_in
{
unsigned short sin_family; #设置AF_ANET
unsigned short sin_port; #要监测的端口号
struct in_addr sin_addr #要监测的IP号(设置INAPDP_ANY
#表示可以和任何的主机通信)
unsigned char sin_zero[8];
}
(3). addrlen 是 sockaddr 结构的长度。
(4).返回值:bind 将本地的端口同 socket 返回的文件描述符捆绑在一起.成功是返回 0, 失败的情况和 socket 一样。
3.listen
此函数宣告服务器可以接受连接请求。(注意:不是表明已经连接成功,listen就像一个人谈恋爱一样。listen之前是,我不想谈恋爱,女人都别来了。listen后,表明我想谈恋爱了,有意向的人可以来了,并是我一想谈恋爱了就有女朋友了)
int listen(int sockfd,int backlog);
(1).sockfd 是 bind 后的文件描述符(也是socket函数返回的文件描述符(句柄))。
(2).backlog 设置请求排队的最大长度。当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度。
(3).listen 函数将 bind 的文件描述符变为监听套接字,返回的情况和 bind 一 样。
4.accept
服务器使用此函数获得连接请求,并且建立连接。
int accept(int sockfd, struct sockaddr *addr,int *addrlen);
(1). sockfd 是 listen 后的文件描述符(及监听套接字)。
(2).addr,addrlen 是用来给客户端的程序填写的,服务器端只要传递指针就可以了, bind,listen 和 accept 是服务器端用的函数。(注:我在看编写网络编程的server程序时,一直很疑惑,服务器是怎么知道要数据发给哪个客户端?后面才想通了,其实就是当有客户端来连接时,会自动把客户IP和端口信息写入该参数中)
(3).返回值 accept 调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接。accept 成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该 描述符写信息了,失败时返回-1。
5.connect
可以用 connect 建立一个连接,在 connect 中所指定的地址是想与之通信的服务器的地址。
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen);
(1). sockfd 是 socket 函数返回的文件描述符。
(2). serv_addr 储存了服务器端的连接信息,其中 sin_add 是服务端的地址。
(3). addrlen 是 serv_addr 的长度
(4). connect 函数是客户端用来同服务端连接的.成功时返回 0,sockfd 是同服务端通讯的文件描述符,失败时返回-1。
6.send
客户或者服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
(1).sockfd 指定发送端套接字描述符(及句柄);
(2).buf 指明一个存放应用程序要发送数据的缓冲区;
(3).len 指明实际要发送的数据的字节数;
(4).flags 一般置 0。
(5).返回值为发送成功的数据有多长。
7.recv
客户或者服务器应用程序都用 recv 函数从 TCP 连接的另一端接收数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
(1). sockfd 指定接收端套接字描述符;
(2).buf 指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;
(3).len 指明 buf 的长度;
(4).flags 一般置 0。
(5).返回值为接收成功的数据有多长。
server.c
include <xxx.h>
......
#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 iReceLen;
unsigned char UCRecvBuf[1000];
int iClientNum = -1;
//socket函数:调动socket函数得到iSocketServer,没有包含任何ID信息
iSocketServer = socket(AF_INET,SOCK_STREAM,0);
if(-1 == iSocketServer)
{
printf(" socket error!\n");
return -1;
}
//bind函数:调用bind,绑定ID和端口,监测指定ID的端口
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = ntoms(SERVER_PORT);
tSocketServerAddr.sin_.S.Addr = INADDR_ANY;
memset(tSocketServer,(const struct Sockaddr *)&tSocketServerAddr,sizeof(struct Socketaddr);
if(-1 == iRet)
{
printf("bind error!\n");
return -1;
}
//listen函数:调用listen,开始监测
iRet = listen(iSocketServer,BACKLOG);
if(-1 == iRet)
{
printf("listen error!\n");
return -1;
}
//调用accept,等待客户端连接
while(1)
{
iAddrLen = sizeof(struct Socketaddr);
iSocketClient = accept(iSocketServer,(struct Sockaddr *)&tSocketClientAddr,&AddrLen);
if(-1 != iSocketClient)
{
iClientNum++;
printf("Get Connect from client %d:%s\n",iClientNum,inet_ntoa(tSocketClientAddr.sin_addr))
if(!fork()) #支持多个客户端,使用fork使调动每个连接
{ #都创建一个子进程
while(1)
{
iRecvLen = recv(iSocketClient,UCReceBuf,999,0); #接收数据
if(iSocketClient <= 0)
{
close(iSocketClient);
return -1;
}
else
{
UCRecvBuf[iRevLen] = '10';
printf("Get Msg From Client %d:%s\n",iClientNum,inet_ntoa(tSocketClientAddr.sin_addr)");
}
}
}
}
}
close(iSocketServer); 关闭
return 0;
}
client.c
#include <xxx.h>
....
int main(int argc,char ** argv)
{
int iSocketClient;
struct SockAddr_in tSocketServerAddr;
int iRet;
unsigned char UCSendBuf[1000];
int iSockLen;
if(argc != 2)
{
printf("Usage:\n");
printf("%s<server_ip>\n",argv[0]);
return -1;
}
iSocketClient = socket(AF_INET,SOCK_STREAM,0);
TSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT);
if(0 == inet_aton(argv[1],&tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero,0,8);
iRet = connet(tSocketClient,(const struct sockaddr*)&tSocketServerAddr,sizeof(struct sockaddr));
if(-1 ==iRet)
{
printf("connect error!\n");
return -1;
}
while(1)
{
if(fgets(UCSendBuf,999,stdin))
{
isendLen = send(iSocketClient,UCSendBuf,strlen(UCSendBuf),0);
if(iSendLen <= 0)
{
close(iSocketClient);
return -1;
}
}
}
}
2.22 UDP编程
UDP网络传输交互图
(1)socket,bind,close,connet函数与TCP用法一样。
(2)发送和接收函数一点变化。
函数补充:
1.sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
(1).sendto 和 send 相似,区别在于 sendto 允许在无连接的套接字上指定一个 目标地址。
(2).dest_addr 表示目地机的 IP 地址和端口号信息.
(3).ddrlen 常常被赋值为 sizeof (struct sockaddr)。
(4).sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
2.recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
(1).recvfrom 通常用于无连接套接字,因为此函数可以获得发送者的地址。
(2).src_addr 是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地 址及端口号。
(3).addrlen 常置为 sizeof (struct sockaddr)。