tcp通信流程分为服务器和客户端的。
这里面有一个比较复杂的就是三次握手和四次挥手,其实这是比较细节的东西,就算不了解也不影响使用的。
TCP(Transport Control Protocol)叫传输控制协议,而UDP(User Datagram Protocol)叫用户数据报协议。TCP和UDP经常被拿来比较,先把名字搞清楚可以比较好区分二者关系。
TCP和UDP共同点就是他们都是协议,的确就是这样,他们都是协议,具体点就是他们都传输层协议。
不同的地方在于,tcp是有链接,可靠的。而udp无连接,不可靠。这个从名字就可以看出来了,一个是传输控制,一个是用户数据报。tcp一看就很严肃,而udp就很随意。
TCP协议
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。
适用情况:
- 适合于对传输质量要求较高以及传输大量数据的通信。
- 在需要可靠数据传输的场合,通常使用TCP协议
- MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP(User Datagram Protocol)用户数据报协议
是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
- 在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络)
- 适合于广播/组播式通信中。
- MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议。
- 流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输。
一个UDP应用可同时作为客户端或服务器。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
总的来说,不用连接的udp是非常好用的,但是与之带来的就是不可靠。这个世界总是如此的守恒。
IP地址
是Internet中主机的标识。
网络通信,肯定是在主机之间的通信,那么就肯定是需要知道是什么主机的了,就好像一个人的身份证一样,而主机的身份证,叫做IP地址。
IP地址是Internet中主机的标识
- Internet中的主机要与别的机器通信必须具有一个IP地址。
- IP地址为32位(IPv4)或者128位(IPv6)。
- 每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由。
表示形式
常用点分十进制形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
知道了ip地址的重要性,那么就需要知道相关的api了,因为网络通信需要使用IP地址,那么就有了对应的关于ip地址的api。
inet_aton()
将strptr所指的字符串转换成32位的网络字节序二进制值。
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in <netinet/in.h> :
struct in_addr {
in_addr_t s_addr; //typedef uint32_t in_addr_t;
};
inet_addr()
功能同上,返回转换后的地址。
int_addr_t inet_addr(const char *strptr);
inet_ntoa()
将32位网络字节序二进制地址转换成点分十进制的字符串。
char *inet_ntoa(stuct in_addr inaddr);
知道了相关函数,到时候就根据需求选择不同的函数就行了。
IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。
这时候就到了字节序的问题了。都知道电脑有小端序和大端序。但是网络通信哪里管你那么多,网络通信直接就说,我就是大端字节序。
这里就要说一下大小端序的定义了。
- 小端序(little-endian) – 低序字节存储在低地址
将低字节存储在起始地址,称为“Little-Endian”字节序;
- 大端序(big-endian)–高序字节存储在低地址
将高字节存储在起始地址,称为“Big-Endian”字节序
(AMD、Intel 采用小端,ARM\Moto 采用大端)
记好笔记,这个要考。
在大部分PC机上,当应用进程将整数送入socket前,需要转化成网络字节序;当应用进程从socket取出整数后,要转化成小端字节序。
就好像IP地址有转换函数一样,字节序也有转换函数。
把给定系统所采用的字节序称为主机字节序。为了避免不同类别主机之间在数据交换时由于对于字节序的不同而导致的差错,引入了网络字节序。
htons()、ntohs()、htonl()和ntohl()。这四个函数分别实现网络字节序和主机字节序的转化。这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。
struct hostent
{
char *h_name;/*正式主机名*/
char **h_aliases;/*主机别名*/
int h_addrtype;/*地址类型*/
int h_length;/*地址字节长度*/
char **h_addr_list;/*指向IPv4或IPv6的地址指针数组*/
}
说完了IP转换函数,就到了MAC地址了,那么MAC地址是什么呢,有了IP地址,怎么又多出来一个MAC地址呢。
MAC地址是Ethernet协议使用的唯一地址,MAC地址又是另一个协议的地址,这个协议毫无疑问是比较重要的,所以才会有这个地址在。
另外说一句,MAC还可以是苹果电脑,有钱的话,我也想体验一下苹果电脑,不知道大家想不想。
MAC地址是Ethernet NIC上自带的,48位长。
如:00-88-D5-03-E7-A8
MAC地址作用范围是Ethernet(局域网)内
MAC地址存在于每一个Ethernet包中,是Ethernet包头的组成部分,Ethernet交换机根据Ethernet包头中的MAC源地址和MAC目的地址实现包的交换和传递。
MAC地址与IP地址无关。
网络通信终于把字节序,IP,mac给介绍完了,这些都是终端所具有的特性。那么怎么在这些之间进行通信呢?那就是使用socket了。
socket
在Linux中的网络编程是通过socket接口来进行的。
socket是一种特殊的I/O接口,它也是一种文件描述符。
socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。
socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。
socket类型
1.流式socket(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
2.数据报socket(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。
3.原始socket(SOCK_RAW)
原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
socket的位置
位于IP和应用层之间。
socket编程
常用函数
socket() 创建套接字
bind() 绑定本机地址和端口
connect() 建立连接
listen() 设置监听端口
accept() 接受TCP连接
recv(), read(), recvfrom() 数据接收
send(), write(), sendto() 数据发送
close(), 关闭套接字
常用头文件
数据类型:#include <sys/types.h>
函数定义:#include <sys/socket.h>
socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,其中根据客户端还是服务器,或者根据使用TCP协议还是UDP协议,这些函数的调用流程都有所区别。
socket()----建立一个socket套接字。可指定socket类型等信息。在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。
bind()----将本地IP地址绑定到端口号。若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。
listen()----监听客户端请求。在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
accept()----接收客户端的请求。服务端程序调用listen()函数创建等待队列之后,调用accept()函数等待并接收客户端的连接请求。它通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。
connect()----TCP客户端与服务器建立连接。该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。
send()/recv()----向一个已连接的socket发送/接收数据。这两个函数分别用于发送和接收数据,可以用在TCP中,在connect()函数建立连接之后再用。
sendto()/recvfrom()----UDP的无连接环境数据的发送与接收。当用在UDP时,可以用在之前没有使用connect()的情况下,这两个函数可以自动寻找指定地址并进行连接。
socket()
应用示例
TCP:sockfd = socket(AF_INET,SOCK_STREAM,0);
UDP:sockfd =socket(AF_INET, SOCK_DGRAM,0);
socket地址信息数据结构
在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14];
/*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
struct sockaddr_in
{
short int sin_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};
struct in_addr {
in_addr_t s_addr; /*in_addr_t为 32位的unsigned int,该无符号整数采用大端字节序。*/
};
注意:sin_addr.s_addr填本机IP,如果此项填INADDR_ANY时,表示自动取本机IP填入该项(仅用于Server)
bind()
该函数是用于将本地IP地址绑定到端口号,若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。
完成此步,该socket拥有了本地IP地址,端口,通信协议,不能接收客户端的请求,但可以向服务器发起连接。
listen()
在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
接收队列
一个新的client的连接请求先被放在接收队列中,等待server程序调用accept函数接受连接请求。
backlog指的就是接收队列的长度,也就是在server程序调用accept函数之前最大允许的连接请求数,多余的连接请求将被拒绝。
accept()
accept()函数将响应连接请求,建立连接产生一个新的socket描述符来描述该连接,这个连接用来与特定的client交换信息。
listen()和accept()是TCP服务器端使用的函数
accept()函数应用示例
struct sockaddr_in their_addr; /* 用于存储连接对 方的地址信息*/
int sin_size = sizeof(struct sockaddr_in);
… …(依次调用socket(), bind(), listen()等函数)
new_fd = accept(sockfd, &their_addr, &sin_size);
printf(”对方地址: %s\n", inet_ntoa(their_addr.sin_addr));
… …
connect()
该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。
然后就是sent()/recv()
sent()
send缺省是阻塞函数,直到发送完毕或出错。
recv()
recv缺省是阻塞函数,直到接收到信息或出错。
sendto()
sendto缺省是阻塞函数,直到发送完毕或出错.
recvfrom()
recvfrom是阻塞函数,直到接收到信息或出错。
close()
套接字的关闭
int close(int sockfd);
关闭双向通讯
shutdown()
Shutdown()函数可以单方面的中断连接,即禁止某个方向的信息传递。
函数调用
int shutdown(int sockfd, int how);
参数how:
0 - 禁止接收信息
1 - 禁止发送信息
2 - 接收和发送都被禁止,与close()函数效果相同。
返回0表示调用成功,返回-1表示出错。
tcp服务器端流程
tcp客户端流程
使用TCP协议的流程图
tcp的三次握手和四次挥手
UDP客户端流程
UDP服务器流程
TCP/UDP编程的差异
- socket()的参数不同
- UDP Server不需要调用listen和accept
- UDP收发数据用sendto/recvfrom函数
- TCP:地址信息在connect/accept时确定
- UDP:在sendto/recvfrom函数中每次均需指定地址信息
- UDP:shutdown函数无效
网络高级编程
两种解决I/O多路复用的解决方法。
select()
使用fcntl()函数虽然可以实现非阻塞I/O或信号驱动I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的CPU资源的占用。在这里可以使用select()函数来解决这个问题,同时,使用select()函数还可以设置等待的时间,可以说功能更加强大。
应用于多路同步I/O模式
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
numfds是要多路选择的socket的最大值。
readfds, writefds, exceptfds都是socket集合,分别代表有数据可读、有数据要写、发生异常的socket集合。
timeout是select的时间限制。
返回值:在socket集合中准备好的socket个数。
socket集合
集合变量类型:fd_set
集合变量运算宏:
FD_ZERO(*set) 清空socket集合
FD_SET(s, *set) 将s加入socket集合
FD_CLR(s, *set) 从socket集合去掉s
FD_ISSET(s, *set) 判断s是否在socket集合中
常数FD_SETSIZE:集合元素的最多个数
select应用举例
fd_set rset; /* 可读的socket集合 */
FD_ZERO(& rset); /* 清空rset集合 /
FD_SET(sockfd, rset); / 将sockfd 加入rset集合 */
nready = select(maxfd+1, &rset, null, null, null);
/* maxfd是已知的最大的socket */
if (FD_ISSET(sockfd, &rset)) {
……
}
ioctl()
获得或改变socket的I/O属性
int ioctl(int sockfd, long cmd, unsigned long* argp)
cmd:属性
argp:属性的参数(缓存区指针)
常用的属性
FIONREAD,返回socket缓冲区中未读数据的字节数
FIONBIO,argp为零时为阻塞模式,非零时为非阻塞模式
SIOCATMARK ,判断是否有未读的带外数据(仅用于TCP协议),返回true或false