一、主机字节序和网络字节序
主机字节序也称小端字节序:整数的高位字节存储在内存的高地址处,低位字节存储在内存的低地址处
网络字节序也称大端字节序:整数的高位字节存储在内存的底地址处,低位字节存储在内存的高地址处
二、检查机器的字节序
小字节序(Little Endian)
DATA:0x0201
总结:低地址存放低位数据
大字节序(Big Endian)
DATA:0x0201
总结:低地址存放高位数据
字节序的支持跟CPU的设计有关,不同架构的CPU可能具有不同的字节序,常见的intel芯片,大多是小字节序
而PowerPC架构多是大字节序的芯片。
测试机器字节序代码:
#include <stdio.h>
union
{
short value1;
char value2[sizeof(short)];
}text;
int main()
{
text.value1 = 0x0102;
if(text.value2[0] == 1 && text.value2[1] == 2)
{
printf("big ending\n"); //大端字节
}
else if(text.value2[0] == 2 && text.value2[1] == 1)
{
printf("short ending\n"); //小端字节
}
else
{
printf("unknown ...");
}
return 0;
}
执行结果
三、Linux字节序转化函数
htonl (长整型32位) 主机字节序转网络字节序
ntohl (长整型32位)网络字节序转主机字节序
htons (短整型16位) 主机字节序转网络字节序
ntohs(短整型16位)网络字节序转主机字节序
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
short A=88;
int B=65536;
short C;
int D;
C=htons(A);
printf("%d htons = %d\n",A,htons(A));
printf("%d ntohs = %d\n",C,ntohs(C));
D=htonl(B);
printf("%d htonl = %d\n",B,htonl(B));
printf("%d ntohl = %d\n",D,ntohl(D));
return 0;
}
四、IP地址转换
通常我们使用点分十进制来表示IP地址,但编程中我们需要把它们转化为整数才能使用,而记录日志时则相反,
我们需要把整数表示的IP地址转化为点分十进制。
头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* 用于将点分十进制表示的IPV4地址转化为网络字节序整数表示的地址*/
in_addr_t inet_addr(const char *str);
/*用于转换整数表示的网络字节排序地址为点分十进制的字符串。该字符串的空间为静态分配的,这意味着在第二
次调用该函数时,上一次调用时将会被重写(复盖)*/
char *inet_ntoa(struct in_addr_in);
EPE:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define IP1 "192.168.47.128"
#define IP2 "192.168.1.1"
int main()
{
struct in_addr A;
A.s_addr = 65535;
printf("IP1 is %s change to IP1:=%d\n",IP1,inet_addr((const char *)IP2));
printf("IP2 is %s change to IP2:=%d\n",IP2,inet_addr((const char *)IP1));
printf("A.s_addr is %d change to IP3:=%s\n",A.s_addr,inet_ntoa(A));
A.s_addr = 16885952;
printf("A.s_addr is %d change to IP4:=%s\n",A.s_addr,inet_ntoa(A));
return 0;
}
REL:
五、Linux socket(套接字)
套接字:TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。
Linux中的网络编程通过Socket(套接字)接口实现。
Socket是一种文件描述符,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
1.流式套接字(SOCK_STREAM)
定义一种可以提供可靠的、面向连 接的通讯流,它使用了TCP协议。
2.数据报套接字(SOCK_DGRAM)
定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的不保证可靠,无差错,UDP协议 。
3.原始套接字
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议的测试等。
TCP/IP专用socket结构体
sa_family_t 地址族类型地址族类型通常与协议族类型相对应,AF_INET表示TCP/IPv4协议族
PF_INET6 表示TCP/IPv6协议族。
SOCKET——实现步骤
1.创建socket——socket()
2.绑定socket——bind()
首先将整个结构体清零,使用bzero()或memset
***函数格式 void *memset(void *s, int ch, size_t n)
函数解释:将s中前n个字节替换为ch并返回s;是用来在在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清 零操作的一种最快方法。
***函数格式void bzero(void *s, int n);
函数解释:s要置零的数据的起始地址
n要置零的数据字节个数,用来置字节字符串s的前n个字节为零且包括‘\0’且无返回值。
3.监听socket——listen()
函数原型: int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字(套接字)
第二个参数为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
4.接受socket——accept()
5.发起连接——connect()
6.关闭连接——close()
7.socket TCP数据读写——recv()、send()、recvfrom() 、sendto()
Socket的send函数的执行流程。当调用该函数时,
(1)send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
(2)如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议s的发送缓冲中的数据是否正在发送,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len
(3)如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完
(4)如果len小于剩余 空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
Socket的recv函数的执行流程。当应用程序调用recv函数时,
(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
其他的形式
•read()/write()
•readv()/writev()
•recvmsg()/sendmsg()
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数
将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有两种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
recv函数和send函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。前面的三个参数和read、write函数是一样的。第四个参数可以是0或者是以下组合:
MSG_DONTROUTE:不查找表,是send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。
MSG_OOB:表示可以接收和发送带外数据。
MSG_PEEK:查看数据,并不从系统缓冲区移走数据。是recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有个进程读写数据的时候使用这个标志。
MSG_WAITALL:等待所有数据,是recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。
8.select()函数
connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
函数声明:
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout)
Maxfd: 文件描述符的范围,比待检的最大文件描述符大1
Readfds:被读监控的文件描述符集
指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这文 件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
Writefds:被写监控的文件描述符集
指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
Exceptfds:被异常监控的文件描述符集
Timeout:定时器
select的超时时间,它可以使得select进入三种工作状态
一、Timeout取不同的值,该调用有不同的表现:
1.Timeout值为0,不管是否有文件满足要求,都立刻返回,无文件满足要求返回0,有文件满足要求返回一个正值。
2. Timeout为NULL,select将阻塞进程,直到某个文件满足要求
3. Timeout值为正整数,就是等待的最长时间,即select在timeout时间内阻塞进程
二、Select调用返回时,返回值有如下情况:
1. 正常情况下返回满足要求的文件描述符个数;
2. 经过了timeout等待后仍无文件满足要求,返回值为0;
3. 如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4. 如果出错,返回-1并设置相应的errno
三、系统提供了4个宏对描述符集进行操作:
#include <sys/select.h>
void FD_SET(int fd, fd_set *fdset) 宏FD_SET将文件描述符fd添加到文件描述符集fdset中
void FD_CLR(int fd, fd_set *fdset) 宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
void FD_ZERO(fd_set *fdset) 宏FD_ZERO清空文件描述符集fdset;
void FD_ISSET(int fd, fd_set *fdset) 在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化
七、
八、UDP通讯模型
九、服务器类型
在网络程序里面,一般来说都是许多客户对应一个服务器,为了处理客户的请求, 对服务端的程序就提出了特殊的要求。目前最常用的服务器模型有:
循环服务器:服务器在同一个时刻只可以响应一个客户端的请求
并发服务器:服务器在同一个时刻可以响应多个客户端的请求
TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况。
但同时也带来了问题:为了响应客户的请求,服务器要创建子进程来处理,而创建子进程是一种非常消耗资源的操作
1.TCP循环服务器
TCP服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接。算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
process(...);
close(...);
}
TCP循环服务器一次只能处理一个客户端的请求。只有在这个客户的所有请求都满足后, 服务器才可以继续后面的请求。这样如果有一个客户端占住服务器不放时,其它的客户机都不能工作了,因此,TCP服务器一般很少用循环服务器模型的
2.UDP循环服务器
UDP循环服务器的实现方法:UDP服务器每次从套接字上读取一个客户端的请求->处理->然后将结果返回给客户机
socket(...);
bind(...);
while(1)
{
recvfrom(...);
process(...);
sendto(...);
}
因为UDP是非面向连接的,没有一个客户端可以老是占住服务端, 服务器对于每一个客户机的请求总是能够满足
3.TCP并发服务器
并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个 子进程来处理。算法如下:
socket(...);
bind(...);
listen(...);
while(1) {
accept(...);
if(fork(..)==0) {
process(...);
close(...);
exit(...);
}
close(...);
}