1.网络编程的基本概念
1.1什么是套接字
套接字,也叫socket,是操作系统内核的一个数据结构,网络通信说白了也就是进程通信(同一台机器上不同进程或者不同机器上的进程间通信)。在网络中确定两个进程通信需要的是机器的网络地址即IP地址,还需要确定是机器中的那个进程也就是进程号即端口号,套接字必须包含这些信息才能进行通信。
1.2端口号的概念
在网络中端口分两种,一个是物理意义上的端口,例如交换机、路由器上用于线路连接其他设备的接口,还有就是TCP/IP协议栈的端口。在一个机器上端口号的范围是0-65535,10-1024是公用的端口,另外的就属于用户自定义端口了。
2.socket的概念
socket是一种特殊的I/O接口,在Linux下就是文件描述符,符合Linux的“一切皆文件”的哲学,有open -> write/read -> close 等模式来操作。
2.1socket类型
2.2.1 流式套接字(SOCK_STREAM)
用于TCP通信,提供可靠的、面向连接的通信流,使用TCP协议,从而保证了数据传输的正确性和有序型。
字节流服务:
1.数据没有发送与接收的界限
2.发送方发送数据的次数和接收方接收数据的次数没有关系
3.若接收端一次未将数据读取完成,未读取数据会继续保留在接收缓冲区中
2.2.2 数据报socket(SOCK_DGRAM)
用于UDP通信,数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且是不可靠的,使用数据报协议UDP。
数据报服务:
1.发送方发送(sendto)的次数和接收方接收(recvfrom)数据的次数是相等的
2.若一次recvfrom没有将UDP报文段文中的数据读取完成,则剩下的数据会被丢弃
2.1.3 原始套接字(SOCK_RAM)
用于新的网络协议的实现,原始套接字允许对底层协议如IP、ICMP进行直接访问,功能强大但使用不便,主要用于自定义协议的开发。
2.2 socket信息数据结构
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
其中sa_family成员是地址族类型的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family)如表所示
协议表 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET | AF_INET6 | TCP/IPv6协议族 |
sa_data成员用于存放socket地址值。
2.3主机字节序与网络字节序
字节序分为大端字节序和小端字节序。大端值得是一个整数的高位字节(23-31bit)存储在内存的低地址处,地位字节(0-7bit)存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而地址字节则存储在内存的低地址处。现代PC机大多采用小端字节序,手机。网络中使用大端字节序。
Linux提供了4个函数完成主机字节序和网络字节序的转换:
- unsigned long int htonl(unsigned long int hostlong);
- unsigned short int htos(unsigned short int hostshort);
- unsigned long int ntohl(unsigned long int netlong);
- unsigned short int ntohs(unsigned short int netshort);
它们的含义很明确,例如:htonl表示“host to network long”即将长整型的主机号转化为网络字节序数据。
判断字节序方法:
union E//共用体,联合体:共用低地址
{
char a;
short b;
};
int main()
{
union E ua;
ua.b = 0x1234;
if(ua.a == 0x34)
{
printf("小端字节序\n");//PC
}
else
{
printf("大端字节序\n");
}
return 0;
}
2.4 IP地址的转化
通常在表达地址是采用点分十进制表示的数值,而在socket编程中中使用的则是32位的网络字节序的二进制值,所以我们需要进行转化。
int inet_aton(const char *straddr, struct in_addr *addrptr);
将点分十进制的IP地址转化为网络字节序的32位二进制数值
char *inet_ntoa(struct in_addr inaddr);
将网络字节序的32位二进制数值转换为点分十进制的IP地址
in_addr_t inet_addr(const char *straddr);
功能与inet_aton相同,传递方式不同,调用成功返回32位二进制的网络字节序地址
2.5 域名与IP地址之间的对应
在网络中另一个繁琐的问题是过长的IP地址使使用的人记不住,所以现在也就有了域名系统,一个域名就对应一个IP地址下面我来讲讲域名与IP地址之间的转化。
在Linux中最常用的两个函数是gethostbyname()和gethostbyaddr(),它们都可以实现IPv4/IPv6的地址和主机名之间的转换,其中gethostbyname是将主机名转化为IP地址,gethostbyaddr是将IP地址转化为主机名。
struct hostent *gethostbyname(const char* hostname);
struct hostent *gethostbyaddr(const char* addr, size_t len, int family);
struct hostent
{
char *h_name;/*正式主机名*/
char **h_aliases;/*主机别名*/
int h_addrtype;/*主机IP地址类型 IPv4为AF_INET*/
int h_length;/*主机IP地址字节长度,4字节32位*/
char **h_addr_list;/*主机的IP地址列表*/
}
3. TCP和UDP的基本函数调用
TCP服务器编程
3.1 TCP编程基本步骤
服务器端:
socket
bind
listen
while(1)
{
accept
recv
send
close
}
close
客户端:
socket
connect
send
recv
close
3.2 socket函数
函数原型:
int socket(int domain, int type, int protocal);
/*
domain:协议簇
type:具体协议类型,TCP/UDP
protocol:下层的具体协议 TCP/IP
*/
功能:生成一个套接字描述符,第三个参数默认值为0时会根据type的值,自动选择协议类型。
返回值:成功返回套接字描述符,失败返回-1
3.3 bind函数
函数原型:
int bind(int listened, struct sockaddr* seraddr,socklen_t len);
struct sockaddr_in
{
sa_family_t sin_family;/*地址簇*/
u_int16_t sin_port;/*端口号*/
struct in_addr sin_addr;/*IPv4地址结构体*/
}
struct in_addr
{
u_int32_t s_addr;/* IPv4地址,使用网络字节序表示*/
}
功能:用来绑定一个端口号和IP地址,使套接字与指定的端口号的IP地址相关联
返回值:成功返回0,失败返回-1,注意这里的失败原因一般是IP地址错误、端口号被占用或没权限使用端口号。
3.4 listen函数
函数原型:
int listen(int sockfd, int backlog)
/*
sockfd是前面socket的返回值;
backlog指定未完成连接队列和已完成连接队列的总和
*/
对backlog参数的详述:
unix内核会为任何一个给定的监听套接字维护两个队列
1.未完成连接队列,服务器收到了客户端的SYN,等待三次握手的完成,这些套接字处于SYN_RECV状态。
2.已完成连接队列,已经完成三次握手,套接字处于ESTABLISHED状态,等待被accept函数提取。
在Linux上的情况略有不同:
1.未完成连接队列,最大限制值由/proc/sys/net/ipv4/tcp_max_syn_backlog决定。
2.已完成连接队列,最大限制值由/proc/sys/net/core/somaxconn决定默认值128。
3.如果backlog参数大于/proc/sys/net/core/somaxconn,则会发生截断,取值/proc/net/core/somaxconn。
例如nginx的backlog默认值是511,系统默认限制是128,那么最终取值128。
功能:是服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。
返回值:成功返回0;失败返回-1。
3.5 accept函数
函数原型:
int accept(int listendfd, struct sockaddr* cli_addr, socklen_t *len);
功能:接收客户机的连接请求,并建立起通信连接,从已经完成连接的队列中获取到连接请求。当accept函数接受一个连接时,会返回一个新的socket标识符,数据的传输和读取通过这个新的标识符来处理,原来的socket继续监听其他客户机的请求。注意:accept函数只是从监听队列中取出连接,而不论连接处于什么状态,网络状况有什么变化。
返回值:成功返回新的socket描述符,失败返回-1。
3.6 recv函数
函数原型:
ssize_t recv (int sockfd, void *buf, size_t len , int flags);
功能:sockfd是accept的返回值,buf表示当前缓冲区,接收远端主机传来的数据,并将数据存到由buf指向的内存空间,recv<0或者=0,意味着对方已经关闭了连接。
3.7 send函数
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据给远端主机,sockfd是accept的返回值,buf是发送数据的缓冲区,send>0成功并返回所发送数据的字符数,可能少于你所指定的发送长度,send=0则表示连接关闭。
3.8 close函数
函数原型:
int close(int sockfd);
功能:当使用完文件时则可以使用close()关闭该文件,在多进程服务器中,会有多个客户端共享一个套接字,所以当一个进程close时,仅将该socket的引用计数减少,当引用计数为0时,才会发生四次挥手。
TCP客户端编程
connect函数
函数原型:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
功能:用来请求连接远程服务器,将参数的socket连接至参数serv_addr指定的服务器IP和端口号上,成功返回0,失败返回-1。
服务器代码:
客户端代码:
存在缺点
多线程处理
使用以上处理方法可以实现多个客户端连接一个服务器,但实际上是阻塞模式(一个客户端访问的时候会阻塞其他客户端使其他客户端不可以访问),使用多线程可以解决此问题。
I/O复用
使用I/O多路复用也可以解决客户端阻塞的问题。
select详解
poll详解
epoll详解
UDP编程
3.9 UDP编程基本步骤
服务器:socket—bind—recvfrom----sendto----close
客户端:socket—sendto----recvfrom----close
3.11 sendto函数
函数原型:
ssize_t sento(int sockfd, const void* buf, size_t len, int falgs, const struct sockaddr *dest_addr, socklen_t addrlen);
功能:往sockfd上写数据,buf和len分别表示缓冲区位置和大小,dest_addr参数指定接收端的sock地址。
3.12 recvfrom函数
函数原型:
ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t addrlen);
功能:读取sockfd的数据,buf和len分别表示缓冲区位置和大小,每次读取数据都需要获取发送端socket地址,及参数src_addr所指内容。
代码:
关于阻塞和非阻塞recv/send
阻塞非阻塞概念:阻塞就是让点前的调用线程一直处于等待当中,挂起的状态,线程会睡眠;非阻塞是不管运行结果如何,都会继续向下执行,往往要处理很多结果。
send/sendto都是将数据拷贝到内核的发送缓冲区,并不保证数据一定会被发送,所以send/sendto函数成功返回了,也仅是代表拷贝成功了。
阻塞模式:
send操作将会等待所有数据都被拷贝到发送缓冲区才会返回,若发送数据长度>缓冲区大小,send会等待直到缓冲区的大小足够时才会返回。
sendto操作则不会阻塞,原因是UDP没有真正意义上的缓冲区,它只是将应用层的数据加上UDP头部信息,IP头部信息,不存在阻塞。
非阻塞模式:
send会在非阻塞模式下立即返回,若缓冲区可用大小为0,则返回EWOULDBLOCK错误,表示无法拷贝任何数据到缓冲区,若发送缓冲区的大小不为0,且小于要发送数据的大小则会将不会数据拷贝到发送缓冲区,由此可以看出,非阻塞的send函数是尽力而为,尽最大能力往发送缓冲区拷贝数据。
sento操作不会阻塞,原因同上。
recv/recvfrom函数是将内核接收缓冲区的数据拷贝到应用层的buf当中,真正的接收数据是由系统层决定的。
阻塞模式:
recv/recvfrom会一直阻塞到接收缓冲区有一个字节或者完整的UDP数据报为止,然后返回,可见接收到的数据可能不是预料的长度len,需要多次读取。
非阻塞模式:
recv/recbfom会立即返回,有数据则返回数据的大小,若无数据,则返回EWOULDBLOCK错误。
信息来源
两种协议的选择
- 对数据要求高可靠性的应用采用TCP协议,例如验证、密码的传送等数据,对数据可靠性不高的可以采用UDP传送。
- TCP协议要进行三次握手、超时重传机制等手段来保证数据传输的可靠性,所以有较大延时,不合适实时性的应用。
- TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷