310 likes | 829 Views
Socket 编程 ( 上 ). Socket programme. 课程目标. Socket 编程基本知识 Socket 常用函数 C/S 结构 TCP 编程. Socket 简介. BSD Socket 接口是 TCP/IP 网络的 API 。 在 Linux 、 Unix 和 Windows 中均实现了这个接口。 BSD Socket 是目前开发网络应用的主要接口,绝大部分网络应用均 可用 Socket 来开发。 一个 Socket 队列是 IP 应用的基本单位,两个机器通讯相当于两 个机器的两个 Socket 互相通讯的过程。
E N D
Socket 编程(上) Socket programme
课程目标 • Socket编程基本知识 • Socket常用函数 • C/S结构 • TCP编程
Socket简介 • BSD Socket接口是TCP/IP网络的API。 • 在Linux、Unix和Windows中均实现了这个接口。BSD Socket是目前开发网络应用的主要接口,绝大部分网络应用均 可用Socket来开发。 • 一个Socket队列是IP应用的基本单位,两个机器通讯相当于两 个机器的两个Socket互相通讯的过程。 • Socket的本意是插座,每一个激活的Socket可以看成是一个跟 本地某个IP端口绑定的IP包队列。 • Socket接口最先是在Unix操作系统中实现的,一个激活的 Socket被设计成特殊的I/O文件,因此Socket也是一种文件描 述符,对Socket的操作类似对一个普通文件的操作。
Socket如何表示IP地址 • IP协议规定:ipv4地址占4个字节,ipv6地址占16字节。所有形式的地址最终都要转 换成这种格式,socket库才能处理。 • 但socket库的地址结构的出发点是试图兼容和处理不同网络的地址形式,而不是仅仅 针对IP地址,因此被设计成一个复杂的联合结构,看起来不好处理。以下是通用地址 结构,用sa_family来指示是哪种类型的网络地址。 struct sockaddr { unsigned short sa_family; /* 地址族,AF_xxx */ char sa_data[14]; /* 14字节的协议地址 */ }; • sa_family为AF_INET表示ipv4地址,此时sockaddr的定义变成如下形式。ipv4大 部分情况下处理struct sockaddr_in,struct sockaddr_in定义在netinet/in.h中。 struct sockaddr_in { short int sin_family; /* 地址族 */ unsigned short intsin_port;/* 端口号 */ struct in_addr sin_addr; /* IP地址 */ unsigned char sin_zero[8]; /* 填充0以保持与struct sockaddr同样大小 */ };
Socket如何表示IP地址(2) • in_addr定义如下: typedef struct in_addr { union { struct {u_char s_b1,s_b2,s_b3,s_b4;}s_un_b; struct {u_short s_w1,s_w2;}s_un_w; u_long s_addr; }s_un; }in_addr; • 这一形式的定义把4个byte的IP地址分成了三种格式,以便各种情况都能处理,实际应用中绝大部分是使用u_long s_addr。 • 综上所述,设置一个ipv4的地址需要对socketaddr_in结构中的三个成员赋值: • sa_family=AF_INET,sin_port设为端口,sin_addr.s_addr设为整数形式的 地址。 由于socketaddr先天性的原因,这个结构被搞得非常复杂,这是开发者要注意的。
其它形式的地址表示 • 人们常用点分表示法的格式表示IP地址,即形如“192.168.0.1” 的字符串表示法格式,但这样的格式需要转成整数才能被socket 库使用,因此socket库提供如下的转换函数: • #include <arpa/inet.h> • intinet_aton(constchar *strptr,struct in_addr *addrptr); • 点分字符串转in_addr结构 • 返回:1—串有效,0—串出错 • in_addr_tinet_addr(constchar*strptr); • 点分字符串转in_addr结构中的u_long型 • 返回:若成功,返回32位二进制的网络字节序地址;若出错,则返回 INADDR_NONE • 类似inet_aton,但inet_addr更为通用 • char*inet_ntoa(structin_addrinaddr); • 将in_addr结构转为点分法字符串 • 返回:指向点分十进制数串的指针 • 注意转换后点分格式是带端口的,比如:202.116.34.194.4000表 示IP为202.116.34.194,端口为4000
其它形式的地址表示(2) • 新版转换函数支持ipv6地址转换 • intinet_pton(intfamily,constchar*strptr,void*addrptr); • constchar*inet_ntop(intfamily,constvoid *addrptr,char*strptr,size_tlen); 函数名中的p表示presentation,即点分地址表示法,如:202.116.34.194。函数名中的n表示numeric数值格式。这两个函数中的端口数字都是网络序。 参见“源代码/tcp/inet_pton.c”,源程序中的“ntohl()”、“htonl()”函数见下页“网络字节序,本机字节序”。
网络字节序,本机字节序 • 不同的计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数在不同的 软硬件系统的内存中存储的方式就不同。字节序分为小端字节序(Little Endian)和大 端字节序(Big Endian)。Intel处理器大多数使用小端字节序,Motorola处理器大多 数使用大端(Big Endian)字节序。 小端字节序:低位字节存储在起始地址(或低地址) 大端字节序:高位字节存储在起始地址(或低地址) • IP协议是适用于不同操作系统及CPU的传输数据的协议,要通过IP协议传输数据就必 须统一规定字节序,否则在传输时会发生混乱。这个统一规定的字节序叫网络序, TCP/IP规定网络序采用大端字节序。相对的, CPU本身的数字表示顺序称为本机 序。 • IP包在发送前必须把本机序转换为网络序,在接收后也需要把网络序转为本机序,这 样才能正常使用。 • 在各种操作系统中都会实现四个转换函数 • include <netinet/in.h> • unit16_t htons(uint16_t host); • unit32_t htonl(uint32_t host); • unit16_t ntohs(uint16_t net); • unit32_t ntohl(uint32_t net);
Socket基本函数 • socket():在本地创建一个Socket,可以认为是创建了一个队列。 • intsocket(intdomain,inttype, intprotocol); • domain:说明网络程序所在的主机采用的通讯协议族,取值有AF_UNIX 和AF_INET。AF_UNIX只能够用于单一的Unix系统进程间通信,而 AF_INET是针对internet的,因此允许与远程主机通信。 • type:网络程序所采用的通讯协议(如:SOCK_STREAM、 SOCK_DGRAM、SOCK_RAW等)。SOCK_STREAM表明采用的是 TCP协议,这样网络会提供按顺序的、可靠的、双向的、面向连接的比特 流。SOCK_DGRAM表明采用的是UDP协议,这样网络只会提供定长 的、不可靠的、无连接的数据通信。SOCK_RAW表示原始的Socket,即 需要用户自行处理IP包头。 • protocol:由于指定了type,所以protocol一般只要用0作为参数值就可 以了。 socket()为网络通讯做基本的准备,成功时返回文件描述符,失败时返 回-1,查看errno可知道出错的详细情况。
Socket基本函数(2) • bind():把一个Socket跟端口绑定,这样Socket才能被外部访 问。 • intbind(intsockfd,structsockaddr*my_addr,int addrlen); • sockfd:socket()返回的文件描述符。 • my_addr:一个指向sockaddr的指针,但一般使用sockaddr_in定义 (见第4页“Socket如何表示IP地址”)来处理ipv4地址。因为主要使用 internet,所以sockaddr_in的sin_family成员一般为AF_INET; sin_port成员是要监听的端口号;sin_addr成员设置为INADDR_ANY 表示可以和任何主机通信;sin_zero[8]只是用来填充。 • addrlen:sockaddr结构的大小,即:sizeof(sockaddr)。 bind将本地的端口同socket()返回的文件描述符捆绑在一起,成功时返回 0,失败时的情况和socket()一样。
Socket基本函数(3) • listen():侦听某个端口。 • int listen(int sockfd,int backlog); • sockfd :socket()返回的文件描述符。 • backlog:当有多个客户端程序和服务端相连时,此参数设置 客户端请求排队的最大长度。 listen()将bind()使用的文件描述符变为监听套接字,返回值 情况和bind()一样。
Socket基本函数(4) • accept():由listen()侦听后,调用此函数等待客户端的连接请求以建立连接。 • int accept(int sockfd, struct sockaddr *addr,int *addrlen); • sockfd:调用listen()后的文件描述符(即:监听套接字)。 • addr、addrlen指向的内存是用来让客户端的程序(一般是 客户端的connect()函数)填写的,服务器端只要传递指针就可以了。 accept()调用时,服务器端的程序会一直阻塞到有一个客户 端程序发出连接。accept()成功时返回一个文件描述符,然 后服务器端可以对该描述符进行读写,失败时返回-1。 bind()、listen()和accept()是服务器端用的函数。
Socket基本函数(5) • connect():联接远程某个Socket。 • int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); • sockfd:socket()返回的用于同服务器端通讯的文件描述 符。 • serv_addr:用于储存服务器端的连接信息,其中的 sin_addr成员存放服务器端的地址。 • addrlen:sockaddr结构的大小,即:sizeof(sockaddr)。 connect()是客户端用来同服务器端建立连接的函数,成功时 返回0,失败时返回-1。
Socket基本函数(6) • read():数据读取,Linux专用函数。 • ssize_tread(intfd,void*buf,size_tnbyte); read()函数负责从文件描述符fd中读取nbytes字节内容到buffer。成功 时,返回实际所读的字节数(返回值为0表示已经读到文件尾);返回值 小于0(返回值一般为-1)表示出现了错误(错误为EINTR说明是由中断 引起的,错误为ECONNREST表示网络连接出了问题)。 • write():数据写入,Linux专用函数。 • ssize_twrite(intfd,constvoid*buf,size_tnbytes); write()函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回 实际所写的字节数,表示写了部分或者是全部的数据;失败时返回-1并设 置errno变量(错误为EINTR表示在写的时候出现了中断错误,错误为 EPIPE表示网络连接出现了问题,如对方已经关闭了连接)。
Socket基本函数(7) • recv()和send():TCP专用收发函数,用于在面向连接的 Socket上进行数据传输。 • intrecv(intfd, void*buf, intlen, unsignedintflags); fd是用于接受数据的Socket的描述符;buf为存放接收到的数据的缓冲 区;len是以字节为单位的缓冲区的长度;flags一般情况下被置为0 (关于该参数的用法可参照man手册)。 recv()返回实际接收的字节数,当出现错误时,返回-1并置相应的errno 值。 • intsend(intfd,constvoid*msg,intlen, unsignedintflags); fd是用于发送数据的Socket的描述符;msg是一个指向要发送的数据的 指针;len是以字节为单位的数据的长度;flags也被置为0。 send()返回实际发送的字节数,可能会小于所希望发送的数据数。在程序 中应该将send()的返回值与欲发送的字节数进行比较。当send()的返回 值与len不匹配时,应该对这种情况进行处理。 • TCP编程中,recv()/send()比read()/write()更有通用性,建 议使用前者。
Socket基本函数(8) • recvfrom()和sendto():UDP专用收发函数,用于在无连接的数据报 Socket上进行数据传输。由于本地Socket并没有与远端机器建立连接,所以 在发送数据时应指明目的地址。 • intrecvfrom(intfd,void*buf,intlen,unsignedintflags,structsockaddr*from, int fromlen); 该函数比recv()函数多两个参数。from指向源机的IP地址及端口号信息,fromlen常被置为 sizeof(structsockaddr)。当recvfrom()返回时,fromlen中为实际存入from指向的结构 中的数据字节数。 recvfrom()函数返回实际接收到的数据字节数或当出现错误时返回-1,并置相应的errno。 • intsendto(intfd,constvoid*msg,intlen,unsignedintflags,const struct sockaddr*to,inttolen); 该函数比send()函数多两个参数。to指向目地机的IP地址和端口号信息,tolen常被赋值为sizeof(structsockaddr)。 sendto函数也返回实际发送的数据字节数或在出现发送错误时返回-1。 当对数据报Socket调用connect()函数时,也可利用recv()和send()进行数据传输,但该 Socket仍然是数据报Socket,并且利用传输层的UDP服务,在实际发送或接收数据报时, 内核会自动为之加上目地和源地址信息。 • UDP编程中,recvfrom()/sendto()比read()/write()更有通用性,建议使 用前者。
Socket 编程模型 • 当前的Socket编程模型一般都是C/S(服务器/客户端)结构,即相互通信的网络程序中,一方称为客户程序(client),另一方称为服务器程序(server)。 • C/S结构中,客户端向服务器发送请求,服务器作出响应。象常见的“浏览器/web服务器”、“FTP客户端/FTP服务器”就是典型的C/S结构。 • 一个服务器可以同时接受多个客户端请求。 • 在Socket编程中,服务器和客户端的编程流程有一些不同。随后的课程将针对TCP、UDP通信编程来教学服务器/客户端的开发流程。
TCP服务器程序编写步骤 • 创建套接口(socket())→绑定套接口(bind())→设置套接口为监听模式,进入被动接受连接请求状态(listen())→接受请求,建立连接(accept())→读/写数据(read()/write()或recv()/send())→终止连接(close())。 参见“源代码/tcp/server.c”。
TCP客户端程序编写流程 • 创建套接口(socket())→与远程服务程序连接 (connect())→读/写数据(read()/write()或 recv()/send())→终止连接(close())。 参见“源代码/tcp/client.c”。
TCP服务器模型(1) • 循环服务器:TCP服务器 • TCP服务器接受一个客户端的连接,然后处理,完成了这个客户端的所有事务后断开 连接。 • socket(...); bind(...); listen(...); while(1) { accept(...); while(1) { read(...); process(...); write(...); } close(...); } • TCP循环服务器一次只能处理一个客户端的请求,只有在这个客户端的所有事务都完 成后,服务器才可以继续后面客户端的请求。这样如果有一个客户端占住服务器不 放,其它的客户机都不能工作。因此,TCP服务器一般很少用循环服务器模型,这种 设计只有参考价值,并无实际意义。
TCP服务器模型(2) • 并发服务器 • 为了弥补TCP循环服务器的缺陷,人们又想出了并发服务器的模型。并发服务器的思想是每一个 客户机的请求并不由服务器直接处理,而是由服务器创建一个子进程来处理。 • socket(...);bind(...);listen(...); while(1) { accept(...); if(fork(...)==0) { while(1) { read(...); process(...); write(...); } close(...); exit(...); } close(...); } • 这个模型采用fork()创建子进程,用一个子进程处理一个客户端请求,效率较高。此模型还可换 成多线程模式,即在fork()处换用pthread_create()来创建线程处理请求。
阻塞与非阻塞 • 阻塞服务中,当服务器运行到accept()语句而没有客户连接服务请求到来,那么会发 生什么情况?这时服务器就会在accept()语句上停止以等待连接服务请求的到来。同 样,当程序运行到接收数据语句recv()时,如果没有数据可以读取,则程序同样会停 止在接收语句上。这种情况称为阻塞(blocking)。 • 如果仅希望服务器注意检查是否有客户在请求连接,有就接受连接,否则就继续做其 它事情,则可以通过将Socket设置为非阻塞方式来实现:非阻塞Socket在没有客户 请求时就使accept()调用立即返回 。 • 通过设置Socket为非阻塞方式,可以实现“轮询”若干Socket。当企图从一个没有数据 等待处理的非阻塞Socket读入数据时,函数将立即返回,并且置返回值为-1,置 errno为EWOULDBLOCK。但是这种“轮询”会使CPU处于忙等待状态,从而降低性 能。考虑到这种情况,如果希望服务器在监听连接服务请求的同时从已经建立的连接 中读取数据,你也许会想到用一个accept()语句和多个recv()语句,但是由于 accept()及recv()都是会阻塞的,所以这个想法显然不会成功。 • 用非阻塞的Socket会大大地浪费系统资源,而调用select()会有效地解决这个问题。 它允许在阻塞时把进程本身挂起来,同时使系统内核监听所要求的一组文件描述符的 任何活动,只要确认在任何被监控的文件描述符上出现活动,select()调用就会返回 “通知”该文件描述符已准备好的信息,从而实现了随机地为进程选择一个可进行的活 动,避免由进程本身测试是否存在可进行的活动而浪费CPU开销。
TCP服务器模型(3) • 单进程并发服务器: I/O多路复用 • I/O多路复用函数 • intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,struct timeval*timeout); 进程在读写文件时有可能被阻塞,直到一定的条件满足。比如从一个套接字读数据时,如果缓 冲区中没有数据可读(通信的对方还没有发送数据过来),读调用就会等待(被阻塞)直到缓 冲区中有数据可读。如果不希望被阻塞,选择之一是使用select()系统调用。只要设置好 select()的各个参数,那么当文件可以读写的时候,select()就返回“通知”可以读写的信息。 参数介绍:nfds 所有需要监控的文件描述符中最大的那一个加1;readfds 所有要读的文件描 述符的集合;writefds 所有要写的文件描述符的集合;exceptfds 其它的服要向我们通知的文 件描述符;timeout 超时设置。 调用select()时进程会一直阻塞直到以下的一种情况发生: 1)有文件可以读;2)有文件可以写;3)超时设置的时间到。 • void FD_SET(int fd,fd_set *fdset); • 将fd加入到fdset中 • void FD_CLR(int fd,fd_set *fdset); • 将fd从fdset里面清除 • void FD_ZERO(fd_set *fdset); • 将所有的文件描述符从fdset中清除 • int FD_ISSET(int fd,fd_set *fdset); • 判断fd是否在fdset集合中
TCP服务器模型(3)续 • socket(...); bind(...); listen(...); while(1) { FD_ZERO(…); select(…); if(FD_ISSET(svr_fd,…)) //如果是服务器侦听套接字被触发,说明一个新的连接请 //求建立 { new_fd = accept(…); //建立新的客户端连接 FD_SET(new_fd,…); //加入到监听文件描述符中去 } else //是一个客户端操作 { //进行read()或者write()操作 } } • 多路复用I/O可以解决资源限制的问题。由于采用select()机制,因此当没有字符可读 时程序处于阻塞状态,这样可最小程度的占用CPU资源。
多路复用流程 • 典型的IO多路复用单 进程并发服务器流程
课堂练习 • echo server采用多进程来实现客户端请求,现要求将处理模型为fork创建的多进程模型改为多线程模型,即每个客户端的处理通过创建一个线程来负责完成。 • 请将File服务器和客户端分布在两台机器上运行,并用TCPDump来监视运行状态。
扩展练习 • TCP客户端或服务器端的编写都有固定的流程,请设计一组通用接口来简化Socket编程开发。 • 一般会有如下接口 • open_tcp_server(short port); • connect_tcp_server(char * ipaddr,short port); 请设计这两组接口。
谢谢,请提问 在疯狂的时代把握未来