Socket网络编程指导 1/37 guoxiaol@mail.ustc.edu.cn 什么是Socket? 2/37 BSD Socket(伯克立套接字)是通过标准的UNIX文件描述符和其它程序通讯的一个方法,目前已经被广泛移植到各个平台。 Socket是独立于具体协议的网络编程接口。在ISO模型中,主要位于会话层和传输层。 Socket的类型 3/37 流式套接字(SOCK_STREAM) 数据报式套接字(SOCK_DGRAM) 原始式套接字(SOCK_RAW) 4/37 Socket所在层次示意图 Application program Stream Socket Interface TCP UDP Datagram Socket Interface Raw Socket Interface IP Physical and data link layers 基本套接字调用 5/37 创建套接字 socket(); 接受连接 accept(); Socket相关的数据结构 6/37 struct sockaddr_in { }; struct in_addr { in_addr_t s_addr; /* 存储32bit 的IP地址*/ } 网络字节顺序和主机字节顺序 7/37 Big-Endian Byte Order:字节的高位在内存中放在存储单元的起始位置 00001010 00010111 00001110 00000110 00001010 00010111 00001110 00000110 Memory Little-Endian Byte Order : 与Big-Endian相反 A A+1 A+2 A+3 8/40 Host byte order( Little-Endian ) 16-bit 32-bit Network byte order(Big-Endian) 16-bit 32-bit htons() ntohs() htonl() ntohl() 网络字节顺序和主机字节顺序的转换 IP地址的转换 9/37 int inet_aton(const char* strptr, struct in_addr *addrptr); 从点状十进制到32位2进制的转换,如“202.38.64.185” 到 11001010,00100110,01000000,10111001 char *inet_ntoa(struct in_addr inadd); 与inet_aton()的功能相反 相关的内存操作函数 10/37 void *memset(void *buffer, int c, int count); 把buffer所指内存区域的前count个字节设置成字符c。 void *memcpy(void *dest, void *src, unsigned int count); 由src所指内存区域复制count个字节到dest所指内存区域。 Void bzero(void *s, int n ); 置字节字符串s的前n个字节为零。 域名和IP地址的转换 11/37 struct hostent *gethostbyname(const char *name); struct hostent { 建立Socket 12/37 int socket(int domain, int type, int protocol); 参数说明: domain:通信使用的协议族,即网络的类型,对于 TCP/IP来说,是AF_INET type: SOCK_STREAM / SOCK_DGRAM protocol: 通常为0 返回整形的socket描述符,如果出错,返回-1 Socket的配置 13/37 Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket()函数时,将建立一个Socket,为一个Socket数据结构分配存储空间。 两个网络程序之间的一个网络连接包括五种信息:通信协议、本地主机地址和端口、远端主机地址和端口。 在使用socket进行网络传输以前,必须配置该socket。 面向连接的socket客户端调用connect()函数在socket数据结构中保存本地和远端信息。 无连接socket的客户端和服务端以及面向连接socket的服务端通过调用bind()函数来配置本地信息。 绑定Socket 14/37 int bind(int sockfd,struct sockaddr_in *my_addr, int addrlen); sockfd是socket()返回的socket描述符; my_addr是指向包含本机IP地址及端口号等信息的 sockaddr类型的指针; addrlen一般被设置为sizeof(struct sockaddr_in)
绑定前sockaddr_in的初始化 15/37 my_addr.sin_family = AF_INET; //选择网络类型为TCP/IP my_addr.sin_addr.s_addr = inet_addr(“210.45.64.222"); my_addr.sin_port = htons( 8888 ); //选择端口8888 addr_len = sizeof(struct sockaddr_in); memset(&my_addr.sin_zero, ‘\0’, sizeof(my_addr.sin_zero)); 建立连接(客户端) 16/37 面向连接的客户程序使用connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为: serv_addr是包含远端主机IP地址和端口号的指针;addrlen是远端地址结构的长度 成功则返回0,出现错误时返回-1 建立连接(服务器端) 17/37 服务器监听端口:listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。 backlog:请求连接队列的最大长度 成功返回0,出错返回-1 建立连接(服务器端) 18/37 accept()函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用accept函数,然后睡眠并等待客户的连接请求。 addr是指向sockaddr_in变量的指针,该变量存放提出连接请求服务的主机的信息 返回新的socket描述符,和请求连接进程的地址联系起来在新的socket描述符上进行数据传输操作。原来的socket继续listen 数据传输(1) 19/37 send()和recv()这两个函数用于面向连接的socket上进行数据传输。 send() 返回实际发送的字节数,可能会少于希望发送的数据。在程序中应该将send()的返回值与欲发送的字节数进行比较。当返回值与len不匹配时,应该进行处理。 数据传输(2) 20/37 recv()函数原型为: buf 是存放接收数据的缓冲区;len是缓冲区的长度。flags也被置为0。 recv()返回实际接收的字节数,当出现错误时,返回-1 数据传输(3) 21/37 sendto()和recvfrom()用于在无连接的数据报socket方 式下进行数据传输。由于本地socket没有与远端机器建立连接,所以在发送数据时要指明目的地址。 sendto()函数原型为: int sendto(int sockfd, const void *buf,int buflen, unsigned int flags, const struct sockaddr_in *to, int tolen); 数据传输(4) 22/37 recvfrom()函数原型为: recvfrom()函数返回接收到的字节数,当出错时返回-1 结束传输 23/37 close()函数用于释放socket,停止在该socket上的任何数据操作: close(sockfd); 也可以调用shutdown() 来关闭该socket 该函数允许只停止某个方向上的数据传输,而一个方向上的数据传输继续进行。 C/S结构 24/37 服务器端要先启动,提供相应服务: 客户端: 流程图 25/37 TCP服务器端 (循环服务器) TCP客户端 socket( ) bind( ) listen( ) accept( ) socket( ) send( ) connect( ) recv( ) recv( ) send( ) close( ) close( ) UDP服务器端 UDP客户端 socket( ) bind( ) listen( ) recvfrom( ) sendto( ) socket( ) bind( ) close( ) close( ) 简单的例子 26/37 int sockfd, newsockfd,addr_len, sendnum; struct sockaddr_in my_addr, their_addr; char * msg = “welcome”; sockfd = socket( AF_INET, SOCK_STREAM, 0 ); //建立socket my_addr.sin_family = AF_INET; //选择网络类型为TCP/IP my_addr.sin_addr.s_addr = inet_addr(“210.45.64.222"); my_addr.sin_port = htons( 8888 ); //选择端口8888 addr_len = sizeof( struct sockaddr_in); memset(&my_addr.sin_zero, ‘\0’, sizeof(my_addr.sin_zero)); bind(sockfd, (struct sockaddr *)&my_addr, addr_len); //绑定socket listen(sockfd,10); //监听,等待连接,等待连接队列最大长度为10 简单的例子(续) 27/37 While( 1 ) { newsockfd = accept( sockfd, (struct sockaddr *)&my_addr, addr_len); sendnum = send(newsockfd, msg, strlen(msg)+1, 0); …… close(newsockfd); } close(sockfd); 阻塞与非阻塞(1) 28/37 阻塞函数:指其完成指定的任务之前不允许程序调用另一个函数,在Windows下还会阻塞本线程消息的发送。 eg: recv( ) ,当socket工作在阻塞模式的时候,如果没有数据的情况下调用该函数,则当前线程会被挂起,直到有数据为止。 非阻塞函数:指操作启动之后,如果可以立即得到结果就返回结果,否则返回表示结果需要等待的错误信息,不等待任务完成函数就返回。 使用非阻塞I/O的方式:select() 例子: while(1){//执行循环 一边输出一边也不要忘了输入 FD_ZERO(&wt_set); FD_ZERO(&rd_set); FD_CLR(s,&wt_set); FD_CLR(s,&rd_set); FD_SET(s,&wt_set); FD_SET(s,&rd_set); timeout.tv_sec = 0; timeout.tv_usec =500000; z=select(s+1,&rd_set,&wt_set,NULL,&timeout); if(FD_ISSET(s,&rd_set)){//有数据可读 z=recv(s,&recvBuff,sizeof recvBuff-1,0); 29/37 阻塞与非阻塞(2) 30/37 在Berkeley socket函数部分中,不涉及网络I/O、本地端工作的函数是非阻塞函数 在Berkeley socket函数部分中,网络I/O的函数是可阻塞函数,也就是它们可以阻塞执行,也可以不阻塞执行。这些函数都使用了一个socket,如果它们使用的socket是阻塞的,则这些函数是阻塞函数;如果它们使用的socket是非阻塞的,则这些函数是非阻塞函数。 并发服务器 31/37 TCP服务器端(并发服务器) socket( ) bind( ) listen( ) accept( ) send( ) recv( ) close( ) fork( ) //派生新进程 close( ) 主进程在accept之后派生新进程,然后主进程继续listen,处理新的连接请求 新进程自行和客户端通信,新进程和主进程抢占CPU WinSock API 32/37 WinSock是一个基于Socket模型的API,在Microsoft Windows操作系统类中使用。 它在Berkeley接口函数的基础之上,还增加了基于消息驱动机制的Windows扩展函数。 Winscok1.1只支持TCP/IP网络,WinSock2.0增加了对更多协议的支持。 Windows下的Socket编程(1) 33/37 和linux下基本相同,需要包含winsock2.h 需要使用Ws_32.lib,可以用以下语句通告程序编译时调用该库: #pragma comment(lib,"Ws2_32.lib") ; WinSock以DLL的形式提供,在调用任何WinSock API之前,必须调用函数WSAStartup()进行初始化,最后,调用函数WSACleanUp()作清理工作。 Windows下的Socket编程(2) 34/37 WSADATA wsd; //设置WINSOCK的版本 WORD wVersionRequested=MAKEWORD(2,2); WSAStartup(wVersionRequested,&wsd) ; //初始化 。。。。。。。。。 WSACleanUp(); Windows下的Socket编程(3) 35/37 MFC提供了两个类CAsyncSocket和CSocket来封装WinSock API,提供了更简单的网络编程接口。 CAsyncSocket在较低层次上封装了WinSock API,缺省情况下,使用该类创建的socket是非阻塞的socket,所有操作都会立即返回,如果没有得到结果,返回WSAEWOULDBLOCK,表示是一个阻塞操作。 Windows下的Socket编程(4) 36/37 CSocket是CAsyncSocket的派生类,缺省情况下使用该类创建的socket是非阻塞的socket,但是CSocket的网络I/O是阻塞的,它在完成任务之后才返回。 CSocket的阻塞不是建立在“阻塞”socket的基础上,而是在“非阻塞”socket上实现的阻塞操作 网络编程作业要求 37/37 不分组,每人独立完成。 基于C/S或P2P结构,使用UDP或TCP协议皆可。 最好使用基本SOCKET API,不反对使用CAsyncSocket和 CSocket类,但不准使用和传输相关的控件。 期末提交设计文档,源码,及可执行文件。 提交时间为12月份,具体提交日期及提交方式待定。 可选题目I BBS发帖程序 通过term方式(202.38.64.3:23)或者通过web方式 完成在test版发一贴的功能(多发会被永久封账号) Bbs账号和密码使用命令行参数或其他方式设置,不要直接写在程序里 对于term方式下的程序,要求能监视程序运行过程(也就是说在程序运行的时候要把服务器的输出打印到屏幕) Referrence: http协议:http://en./wiki/HTTP 38/37 可选题目II 完成一个HTTP服务器 使用HTTP 1.1协议 支持最大至少10个并发连接(fork创建子进程) 要求服务器程序运行以后,能在浏览器中访问文件,正常显示 Web server的根目录使用命令行参数或者其他方式制定,不要写在程序代码里 文件不存在时返回浏览器404错误 39/37 40/37 谢谢 |
|