(1)主机字节序与网络字节序
字节序是指内存多于一个字节类型的数据在内存中的存放顺序。
一种是将低序字节存储在起始地址,这种称为小端(little-endian)字节序。
一种是将高序字节存储在起始地址,这种称为大端(big-endian)字节序。
两种字节序没有标准,两种格式都有系统使用。
只有大于一个字节类型的数据在内存中的存放有顺序,一个字节的数据没有顺序的问题。
主机字节序(host-byte):不同的机器主机字节序不相同,与CPU设计有关,与操作系统无关。
由于不同机器之间对同一个数据的存放顺序不一样,使得不同体系结构之间无法通信,所以在通信时需要将数据转换成一种约定的字节序,即网络字节序。
网络字节序(network-byte):网络字节序是TCP/IP中规定好的一种数据表示格式,其采用大端字(big-endian)节序。与CPU类型、操作系统等无关。
网络字节序和主机字节序之间的转换函数:
1)htons() 和 ntohs() 完成16位(short)无符号数的相互转换。
2)htonl() 和 ntohl() 完成32位(long)无符号数的相互转换。
所以在socket通讯时,指定ip地址(32位)及通讯端口(16位)时都将其做了对应转换。
serveraddr->sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址
serveraddr->sin_port = htons(atoi(argv[1])); // 指定通信端口
(2)sockaddr_in结构体
其原始结构体定义:
struct sockaddr {
unsigned short sa_family; // 地址类型 AF_XXX
char sa_data[14]; // 14字节存放端口和地址
};
此结构存放地址及端口号不方便,于是
结构体定义如下:
struct sockaddr_in {
short int sin_family; // 地址类型 AF_INET(ipv4)
unsigned short int sin_port; // 端口号
struct in_addr sin_addr; // 地址
unsigned char sin_zero[8]; // 为了保持与 struct sockaddr 一样的长度
};
struct in_addr { // 简化定义 详细定义可百度
unsigned long s_addr; // 地址
};
sin_zero[8]为了保持与 struct sockaddr 一样的长度,使得两个结构体可以强制转换。
端口号 sin_port:0不使用
1)[1, 1023]:熟知端口,IANA把这些端口号指派给了TCP/IP体系中最重要的一些应用协议。
2)[1024, 49151]:为没有熟知端口号的应用程序使用。使用这类端口号必须在IANA按规定的手续登记,以防止重复。
3)[49152, 65535]:留给客户进程选择暂时使用。
端口号只具有本地意义,只是为了标识本计算机应用层中的各进程。
(3)gethostbyname()与inet_addr()
1)in_addr_t inet_addr(const char* cp):将字符串形式的ip地址转换为网络字节顺序的整型值。函数成功,返回非0,函数不正确则会返回0。
2)char* inet_ntoa(struct in_addr in):把网络字节序的IP地址转换成字符串的IP地址。
3)gethostbyname()定义如下:
struct hostent *gethostbyname(const char *name);
参数:name,可以是域名或者主机名,例如“192.168.1.3”、“www.baidu.com”等,仅支持ipv4。
返回值:如果成功,返回一个hostent结构指针,失败返回NULL。
hostent结构体定义如下:
struct hosttent {
char* h_name; // 主机规范名
char** h_aliases; // 主机的别名
int h_addrtype; // 表示的是主机ip地址的类型。AF_INET/AF_INET6
int h_length; // 主机ip地址的长度
char** h_addr_list; // 主机的ip地址,网络字节序存储
};
# define h_addr h_addr_list[0]
将获得的ip地址拷贝到sockaddr_in中:
memcpy(&servaddr->sin_addr, h->h_addr, h->h_length);
(3)socket()
socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。
函数声明:
int socket(int domain, int type, int protocol);
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX、Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合,AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TICP传输协议。
常用参数为ipv4,TCP连接,故参数分别为AF_INET、SOCK_STREAM、IPPROTO_TCP(0)。
返回值:成功返回一个socket(int),失败返回-1。错误原因存于errno 中。
(4)bind()
bind函数用于服务端将用于通讯的地址和端口绑定到socket上。
函数声明:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
socket:需要绑定的socket
addr:存放了服务器端用于通信的地址和端口,类型为struct sockaddr *,故使用sockaddr_in结构体定义通信地址和端口时,需强制类型转换
bind(listenfd, (struct sockaddr *) serveraddr, sizeof(*serveraddr))
addrlen:表示addr结构体的大小
返回值:成功则返回0, 失败返回-1,错误原因存于errno中。
如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。
服务端程序的端口释放后可能会处于TIME_WAIT状态,需等待两分钟之后才能再被使用,可以通过设置SO_REUSEADDR属性可以让端口释放后立即再次使用。
// 设置SO_REUSEADDR选项
int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, len);
(5)listen()
listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其他socket的连接请求,从而成为一个服务端的socket。
函数声明:
int listen(int socket, int backlog);
socket:一个已经被bind过的socket。socket()函数返回的是一个主动连接的socket,在服务器端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被顶等待客户端来连接。由于系统默认认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,即使用listen()函数。
backlog:此参数涉及网络细节,一般填5, 10 都行,一般不超过30。
返回值:0成功,-1失败,错误原因存于errno中。
当调用listen之后,服务端的socket就可以调用accept来接受客户端的连接请求。
(6)accept()
accept函数从已准备好的连接队列中获取一个请求,如果队列为空,accept函数将阻塞等待。
函数声明:
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
socket:一个已经被listen过的socket。
addr:用于存放客户端的地址信息,用socketaddr结构体表达,如果不需要客户端的地址,可以填0。
addrlen:用于存放addr参数的长度,如果addr为0,addrlen也填0。
返回值:返回一个新的socket,服务端使用这个新的socket和客户端进行报文的收发。
accept在等待的过程中,如果被中断或其他原因,函数返回-1,表示失败,错误原因存于errno中,可以重新accept。
(7)connect()
connect函数用于客户端向服务端发起连接请求。
函数声明:
int connect(int sockfd, struct sockaddr* serv_addr, int addrlen);
sockfd:用于连接服务器端的socket。
serv_addr:存储有连接服务器地址及端口。
addrlen:结构体sockaddr的长度。
返回值:0成功,-1失败,错误原因存于errno中。
当服务端地址错误、端口错误或服务端没有启动时,connect一定会失败。
小结:
服务端:socket->bind->listen->accept
客户端:socket->connect
1)服务端在调用listen()之前,客户端不能向服务端发起连接请求。
2)服务端调用listen()之后,服务端的socket开始监听客户端的连接。
3)客户端调用connect()函数向服务端发起连接请求。
4)在TCP底层,客户端和服务端握手后建立起通信通道,如果有多个客户端请求,在服务端就会形成一个已准备好的连接的队列。
5)服务端调用accept()函数从队列中获取一个已准备好的连接,函数返回一个新的socket,新的socket用于与客户端通信,listen的socket只负责监听客户端的连接请求。