网络通信
1.socket编程基础
socket通信的三要素
1.通信的目的ip地址
2.使用的端口号
3.使用的传输层协议(如TCP、UDP)
socket通信模型
套接字概念
socket(插座),在linux环境下,用于表示进程间网络通信的特殊文件类型。实际上就是内核借助缓冲区形成的伪文件。
既然是文件,我们就可以用文件描述符引用套接字。linux系统将其封装成文件的目的是为了统一接口,让读写套接字和读写文件的操作一致。区别是文件主要应用于本地持久化数据的读写(本地读写),而套接字多应用于网络进程间数据的传递(网络读写)。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用socket来描述网络连接的一对一关系。
在网络通信中,套接字一定是承兑出现的。一端的发送缓冲区对应端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
socket 通信创建流程图
服务器端,首先创建一个socket,然后绑定自己的IP和端口号(bind),而客户端这一部分由系统处理。接着用listen对端口监听,也是把端口挂出去,用accept来阻塞直到有客户进来。
网络字节序和主机字节序
在计算机世界里,有两种字节序:
1 . Little endian :将低序字节存储在起始地址 2 . Big endian :将高序字节存储在起始地址
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端和小端之分,那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按照内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高保存的,因此,网络数据流的地址应该规定成这样:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。pc大多采用小端字节序,即主机字节序。
为了使网络程序具有可移植性,使同样的代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
头文件:#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t hton2(uint16_t hostshort);
uint32_t ntonl(uint32_t netlong);
uint16_t nton2(uint16_t netshort);
h代表host,n表示network,1表示32位长整数,s代表16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
如何判断自己是大端还是小端字节序
#include <stdio.h>
union
{
char ch;
int i;
}un;
int main(void)
{
un.i = 0x12345678;
if(un.ch == 0x12)
{
printf("big endian\n");
}
else
{
printf("small endain\n");
}
return 0;
}
sockaddr数据结构
socket网络接口中表示socket的地址。
sockaddr
头文件:#include <sys/socket.h>
struct sockaddr {
sa_family_t sin_family;*//地址族*
char sa_data[14]; *//14字节,包含套接字中的目标地址和端口信息*
};
协议族对应的地址族表。 协议族及其地址值。
sockaddr_in
头文件:#include<netinet/in.h>或#include <arpa/inet.h>`
struct sockaddr_in
{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
}
该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址
struct in_addr{
uint32_t s_addr;
}
总结
struct sockaddr 很多网络编程使用地都是IPv4协议,那个时候都用的sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void*)的作用,传递一个地址给函数,由地址族决定是sockaddr_in还是啥的,然后函数内部强制类型转化为所需的地址类型。
IP地址转换函数
Linux下头文件:<arpa/inet.h>
int iner_pton(int af, const *src, voud *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af取值可选位AF_INET6和AF_INET, 即ipv6和ipv4。因此函数接口是void *addrptr。
便于理解的一个函数。
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
char ip[]="2.3.4.5";
char server_ip[64];
struct sockaddr_in server_addr;
inet_pton(AF_INET, ip, &server_addr.sin_addr.s_addr);
printf("s_addr : %x\n", server_addr.sin_addr.s_addr);
printf("s_addr from net to host: %x\n", ntohl(server_addr.sin_addr.s_addr));
inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, server_ip, 64);
printf("server ip: %s\n", server_ip);
return 0;
}
输出结果:
2.Socket 编程函数
socket 函数
socket是可读、可写、可控制、可关闭的文件描述符(linux一切皆文件)。
int socket(int domain, int type, int protocol);
domain:即协议域,又称为协议族(family)。常用的协议族有:
? AF_INET 这是大多数用来产生socekt的协议,使用TCP或UDP来传输,用IPv4地址。
? AF_INET6 与上面类似,不过是用IPv6的地址。
? AF_LOCAL(或称AF_UNIX,Unix域socket) 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。
type:
? SOCK_STREAM 这协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP协议。
? SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议不可靠,使用UDP来进行它的连接。
? SOCK_SEQPACKET 该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
? SOCK_RAW socket类型提供单一的网络访问,这socket类型使用ICMP公共协议。(比如ping、traceroute)
? SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
? 传0表示使用默认协议。
返回值:
? 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno。
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1.对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则typr参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
bind 函数
命名socket,也就是将socket和socket地址绑定。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
? socket 文件描述符,由socket()函数创建,唯一标识一个socket。
addr:
? 构造出IP地址加端口号
addrlen:
? sizeof(addr)长度,也就是地址长度
返回值:
? 成功返回0,失败返回-1,设置errno
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述监听 addr 所描述的地址和端口号。sturct sockaddr * 是一个通用指针类型,addr 参数实际上可以接受多种协议的sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数addr len指定结构体的长度 如:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(666);
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为666(自己设置的)。
listen函数
int listen(int sockfd, int backlog);
sockfd:
? socket 文件描述符
backlog:
? 在Linux系统中,它是指排队等待建立3次握手队列长度
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
改变系统限制的backlog 的大小
vim /etc/sysctl.conf
net.core.somaxconn = 1024
net.ipv4.tcp_mac_syn_backlog = 1024
sysctl -p
典型的服务器可以同时服务多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0, 失败返回-1。
accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
? socket文件描述符
addr:
? 传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
? 传入传出参数,传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
? 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno。
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上。
connect 函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
? socket 文件描述符
addr:
? 传入参数,指定服务器端地址信息,含IP地址河端口号
addrlen:
? 传入参数,传入sizeof(addr)大小
返回值:
? 成功返回0,失败返回-1,设置errno
客户端需要调用connect()连接服务,connect河bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
出错处理函数
系统函数调用不能次次成功,必须做出出错处理,一方面可以保证逻辑正常,另一方面可以迅速得到故障信息。
头文件: errno.h
函数如下:
char *strerror(int errnum)
errnum:
? 传入参数,错误编号的值,一般取errno的值
返回值:
? 错误原因
关闭连接
直接用关闭普通文件描述符的系统调用就可以
int close(int fd);//fd是待关闭的socket
不过,在多进程中,一次fork会将父进程中的socket引用加1,而close只能将引用减1,所以文明需要在父进程和子进程中都执行close才行。
当然也有直接终止连接的(只为网络编程设计)
int shutdown(int sockfd, int howto);
howto参数可选值:
|