Linux中的网络编程通过Socket(套接字)接口实现,Socket是一种文件描述符。
套接字socket有三种类型:
• 流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP保证了数据传输的正确性和顺序性。
•数据报套接字(SOCK_DGRAM)数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错,它使用数据报协议UDP。
•原始套接字
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议的测试等。
地址结构
struct sockaddr
{
u_short sa_family;
char sa_data[14];
}
Sa_family:地址族,采用“AF_xxx”的形式,如:AF_INET。Sa_data:14字节的特定协议地址。
地址结构
struct sockaddr_in{
shortintsin_family; /*Internet地址族*/
unsignedshortintsin_port; /*端口号*/
struct in_addr sin_addr; /* IP地址*/
unsignedcharsin_zero[8]; /*填0*/
}编程中一般并不直接针对sockaddr数据结构操作,而是使用与sockaddr等价的sockaddr_in
struct in_addr{
unsigned long s_addr;
}
S_addr: 32位的地址。
地址转换
IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct
in_addr中使用的是IP地址是由32位的整数表示的,为了转换我们可以使用下面两个函数:
int inet_aton(const char *cp,struct in_addr *inp)
char *inet_ntoa(struct in_addr in)
函数里面a代表ascii n代表network.第一个函数表示将a.b.c.d形式的IP转换为32位的IP,存储在inp指针里面。第二个是将32位IP转换为a.b.c.d的格式。
字节序转换
32bit的整数(0x01234567)从地址0x100开始:
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
htons把unsigned
short类型从主机序转换到网络序
htonl把unsigned long类型从主机序转换到网络序
ntohs把unsigned short类型从网络序转换到主机序
ntohl把unsigned long类型从网络序转换到主机序
IP与主机名
在网络上标识一台机器可以用IP,也可以使用主机名。
struct hostent *gethostbyname(const char *hostname)
struct hostent{
char *h_name;/* 主机的正式名称 */
char *h_aliases;/* 主机的别名 */
int h_addrtype;/* 主机的地址类型 AF_INET*/
int h_length;/* 主机的地址长度 */
char **h_addr_list;/* 主机的IP地址列表 */
}
#defineh_addrh_addr_list[0] /*主机的第一个IP地址*/
地址转换
IP地址通常由数字加点(192.168.0.1)的形式表示,而在struct in_addr中使用的是IP地址是由32位的整数表示的,为了转换我们可以使用下面两个函数:
int inet_aton(const char *cp,struct in_addr *inp)
char *inet_ntoa(struct in_addr in)
函数里面a代表ascii n代表network.第一个函数表示将a.b.c.d形式的IP转换为32位的IP,存储在inp指针里
面。第二个是将32位IP转换为a.b.c.d的格式。
进行Socket编程的常用函数有:
•socket
创建一个socket。
•bind
用于绑定IP地址和端口号到socket。
•connect
该函数用于绑定之后的client端,与服务器建立连接。
listen
设置能处理的最大连接要求,Listen()并未开始接收连线,只是设置socket为listen模式。
accept
用来接受socket连接。
send
发送数据
recv
接收数据
基于TCP-服务器
1.创建一个socket,用函数socket()
-
绑定IP地址、端口等信息到socket上,用函数bind()
-
设置允许的最大连接数,用函数listen()
-
接收客户端上来的连接,用函数accept()
-
收发数据,用函数send()和recv(),或者read()和write()
-
关闭网络连接
基于TCP-客户端
1.创建一个socket,用函数socket()
-
设置要连接的对方的IP地址和端口等属性
-
连接服务器,用函数connect()
-
收发数据,用函数send()和recv(),或者read()和write()
-
关闭网络连接
基于UDP-服务器
- 创建一个socket,用函数socket()
- 绑定IP地址、端口等信息到socket上, 用函数bind()
- 循环接收数据,用函数recvfrom()
-
关闭网络连接
基于UDP-客户端
- 创建一个socket,用函数socket()
- 绑定IP地址、端口等信息到socket上, 用函数bind()
- 设置对方的IP地址和端口等属性
- 发送数据,用函数sendto()
-
关闭网络连接
服务器模型
在网络程序里面,一般来说都是许多客户对应一 个服务器,为了处理客户的请求, 对服务端的程 序就提出了特殊的要求。目前最常用的服务器模 型有:
• 循环服务器:服务器在同一个时刻只可以响应 一个客户端的请求
• 并发服务器:服务器在同一个时刻可以响应多 个客户端的请求
UDP循环服务器
UDP循环服务器的实现方法:UDP服务器每次从套接字上读取一 个客户端的请求->处理->然后将结果返回给客户机。
socket(...);
bind(...);
while(1)
{
recvfrom(...);process(...);
sendto(...);
}
因为UDP是非面向连接的,没有一个客户端可以老是占住服务 端, 服务器对于每一个客户机的请求总是能够满足。
TCP循环服务器
TCP服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接。算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);process(...);
close(...);
}
TCP循环服务器一次只能处理一个客户端的请 求。只有在这个客户的所有请求都满足后, 服务 器才可以继续后面的请求。这样如果有一个客户 端占住服务器不放时,其它的客户机都不能工作 了,因此,TCP服务器一般很少用循环服务器模 型的。
TCP并发服务器
并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是由服务器创建一个 子进程来处理。算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
if(fork(..)==0)
{
process(...);
close(...);
exit(...);
}
close(...);
}
TCP并发服务器 TCP并发服务器可以解决TCP循环服 务器客户机独占服务器的情况。但同时也带来了问题:为了响应客户的请求,服务器要创建子进程来处理,而创 建子进程是一种非常消耗资源的操作。
多路复用I/O
阻塞函数在完成其指定的任务以前不允许程序继续向下 执行。例如:当服务器运行到accept语句时,而没有客 户请求连接,服务器就会停止在accept语句上等待连接 请求的到来。这种情况称为阻塞(blocking),而非阻 塞操作则可以立即完成。例如,如果你希望服务器仅仅 检查是否有客户在等待连接,有就接受连接,否则就继 续做其他事情,则可以通过使用select系统调用来实 现。除此之外,select还可以同时监视多个套接字 。
多路复用I/O
int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout)- Maxfd: 文件描述符的范围,比待检的最大文件描述符大1
- Readfds:被读监控的文件描述符集
- Writefds:被写监控的文件描述符集
- Exceptfds:被异常监控的文件描述符集
-
Timeout:定时器
多路复用I/O Timeout取不同的值,该调用有不同的表现:
Timeout值为0,不管是否有文件满足要求,都立刻返 回,无文件满足要求返回0,有文件满足要求返回一个正值。
Timeout为NULL,select将阻塞进程,直到某个文件满足要求
Timeout值为正整数,就是等待的最长时间,即 select在timeout时间内阻塞进程。
多路复用I/O
Select调用返回时,返回值有如下情况:
1. 正常情况下返回满足要求的文件描述符个数;
2.经过了timeout等待后仍无文件满足要求,返 回值为0;
3. 如果select被某个信号中断,它将返回-1并设 置errno为EINTR。
4. 如果出错,返回-1并设置相应的errno。
多路复用I/O
1. 设置要监控的文件
2. 调用Select开始监控
3. 判断文件是否发生变化
多路复用I/O
系统提供了4个宏对描述符集进行操作: #
include <sys/select.h>
void FD_SET(int fd, fd_set *fdset)void FD_CLR(int fd, fd_set *fdset)
void FD_ZERO(fd_set *fdset)
void FD_ISSET(int fd, fd_set *fdset)
宏FD_SET将文件描述符fd添加到文件描述符集fdset中;
宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
宏FD_ZERO清空文件描述符集fdset;
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化。
多路复用I/O
FD_ZERO(&fds); //清空集合
sock1 = socket(......);
sock2 = socket(......);
bind(sock1,...);
bind(sock2,...);
listen(sock1,...);
listen(sock1,...);
FD_SET(sock1,&fds); //设置描述符 FD_SET(sock2,&fds); //设置描述符 maxfdp=(sock1>sock2?sock1:sock2) + 1;switch(select(maxfdp,&fds,NULL,NULL,&timeout)) case -1: exit(-1);break; //select错误,退出程序 case 0:break;
default:if(FD_ISSET(sock1,&fds)) //测试sock1是否可读 accpet(sock1,...)