在 Windows操作系统下,使用Socket 编程实现两个进程之间的通信,两个进程间使用TCP 建立可靠连接, 熟悉 TCP 协议以及套接字编程原理;两个进程间使用C/S 工作模式,在此结合并发服务器原理,实现服务器同时与多进程之间进行通信,深入了解 C/S 工作模式。 TCP 是传输层的应用协议,可通过多方而来保证数据传输的可靠性, 它采用了停止等待方式来回应数据的接收,只有当接收到数据时,用户才发送数据确认报文,同时此协议使用滑动窗口方法来控制网络拥塞,提高数据传输的可靠性。 基于TCP/IP 协议和套接字编程原理为服务器端完成基本数据配置,主要采用套接字编程原理 ,通过调试运行并且经过完善后,实现并发服务器功的能. 在这次课程设计的过程中,对于网络编程有了更深一步的了解。首先是对于计算机网络基础这方面,对于网络体系中的几种分层方法,每层的作用都有了系统的理解 ,特别是对于 Socket 编程这一块,套接字的创建、绑定以及使用,基于 TCP、UDP 的编程以及并发服务器的原理都有了深一步的理解, 这次课程设计对于以后进行网络的开发有很大的帮助。
本课题目标是实现局域网用户之间的即时交流和文件传输,通过基础的网络 SOKET 编程, 为局域网内部的即时通信提供一 个简单而较安全的解决方案
1.因为TCP是面向连接的,所以在写基于TCP服务器的代码时,要有listen套接字和accept套接字,而基于UDP模型的代码,并且UDP客户端直接调用 recvfrom/sendto 直接通信即可,不用调用connect函数,这也分别体现出了它们的特性TCP面向连接,而UDP则是无需连接。 2.对于read在网络通信中,因为 TCP是基于字节流的,所以每次read上来的数据都是一个数据段。可能你这里发了一个1024字节的数据,到了运输层可能分段,所以对端可能不会一次性对上来1024字节的数据。所以由此看出来read每次读socket的文件的时候,都读的是一个数据段。 3.对于阻塞socket而言,write调用的时候,当我们把应用层的数据拷贝到内核缓冲区的时候,如果内核缓冲区已满,那么就会阻塞,从write 函数返回也并不代表,数据发送到了对端,只是代表了数据已经拷贝到了内核缓冲区。 4.对于UDP来说,因为是不可靠的,所以也就UDP socket也就没有内核缓冲区,网络层加上包头后直接把数据发送到 数据链路层 的 队列中。从write 返回就是代表数据报已经写入到了数据链路队列。如果 我们发的应用层数据大于 SO_SNDBUF(套接字发送缓冲区上限) 会收到 EMSGSIZE 错误,这不像TCP会阻塞。 如果链接队列满了,因为是UDP协议栈向上报错,一直到发送方。UDP 并不会重传,所以内核会向应用层返回一个 ENOBUFS 。 5.内核的TCP发送缓冲区会一直缓存从应用层拷贝到的数据,一直到收到ACK后,才把这些数据丢弃。 6.write成功返回,对于 TCP来讲只是应用层数据被拷贝到了内核发送缓冲区中,对于 UDP来讲只是数据被加入到了 数据链路层队列中。 7.connect调用失败后,并不可以直接重用该socket,重用前需 close再用,因为基于 Tcp状态转换图得知 , connect 函数其实就是三次握手,那么当三次握手失败的话,socket 不是处于 CLOSED 而是SYN_SENT状态,所以我们必须重新关闭才能再次使用该 socket。 8.对于sockaddr 结构体的疑问,为什么这些socket api 不直接使用void* , 而使用这个通用结构体,因为socket api 比 c 语言的 void * 更早出世,所以在当时没有 void* 这一概念的时候就使用了通用结构体来充当void*。
如图3.1socket模型创建流程图:
4.1 TCP server
首先我们需要先有个套接字,这个套接字必须绑定服务器相应的iP地址和port端口号。而且这个套接字需要是listen状态的。那么当有client向TCP发送连接时,服务器进程调用accept函数就可以查看listen的未决连接队列是否有未处理的连接,如果有就创建一个相应client连接的套接字返回,这时我们就可以通过这个accept套接字和client通信了。
步骤: 1 socket调用
2 bind 绑定服务器ip和端口号
3 listen 使该套接字成为监听状态
4 accept 调用拿到与相应cilent绑定的套
4.2 TCP client
首先调用socket 申请套接字,然后调用connect连接server的ip/port,connect返回的这个套接字,就可以与server通信。
4.3系统中是使用的主要函数
4.3.1 int socket (int domain,int type,int protocl)
第一个参数 指网络层是什么类型的协议, ipv4 ipv6 之一类。 第二个参数 指传输的数据流的类型,tcp 为 sock_stream
第三个参数 基于1/2参数选项组合来选的,一般设置为0,让内核为我们选择
4.3.2 int bind(int sockfd,const struct * sockaddr,socklen_t addrlen) 第一个参数为 需要绑定的套接字 第二个参数为 关于socket地址的数据结构,它其中记录了套接字要使用到的ip/port,所以定义出这个结构体就可以跟我们申请的套接字绑定了。(一般网络协议不同套接字地址结构体不同,调用时需要去强转) 第三个参数为 第二个参数类型的大小。它的返回值很特殊,成功返回0。
4.3.3 bind中的IP地址
对于Client端来说,如果我们绑定了IP表明,这个IP是它的源IP。对于Serve端来讲,绑定了IP表明 Server只能接受这个IP上的连接(也就是固定网卡接口了)。 如果我们不自己设置,内核也会为我们设置。让内核来选择随机端口的话,我们只需设置端口号为0即可。对于让内核来选IP地址则,如果是IPV4给其赋值 INADDR_ANY 即可,也就是0。对于IPV6,则需要赋值 in6addr_any 这个是结构体,不过它值也是0,但是我们不能直接用0赋值。 内核选择IP地址的时机是当有一个连接Connect(TCP)也就是三次握手完毕后或者 当一个UDP数据报被 发出去(UDP), 此时内核才会为socket绑定地址。 选择IP地址的方式对于Tcp来说,如果是一个Server(listen态socket),内核是这样的,根据Client端发来的 SYN段中的目的端地址作为 源IP。如果是一个Client,内核会根据要连接的server的路由情况,从各个网卡中选择一个合适的IP地址。
4.3.4 bind中的端口号 (第二个参数为const的缘故不能直接查看ip与port)
当我们设置端口号为0的时候,内核会为我们选择一个随机端口号,由于第二个参数是const的原因,所以调用完毕后,我们无法查看内核为我们选择的端口号,我们只能通过getsocketname来查看。 绑定知名端口号,需要root权限
4.3.5 bind绑定相同的地址
有时我们不想Server主动断开连接后由于time wait,而不能立即重启,所以就想重新
绑定相同的地址的Server。 那么为了端口复用需满足以下条件其中之一: 1.socket绑定的不是同一网卡可以绑定 2.设置了 SO_REUSEADDR并且不能是LINTEN状态的节点 可见如果是处于time_wait的节点,我们只需提前设置 SO_REUSEADDR即可端口
复用,解决Server不能立刻重启。
4.3.6 将ip 与 port 设置为0当我们设置0,并非内核中对应的连接节点的ip与port就是0,只是代表让内核帮
我们选择合适的 ip 与 port 来绑定。
4.3.7 int listen (int sockfd,int backlog) 第一个参数 绑定后的套接字 第二个参数 listen_socket 的 有俩个队列。完全链接队列(状态为ESTABLEISHED)与 半完全链接队列。backlog 参数是用来设置完全连接队列大小的参数,如果设置为0 ,
根据不同平台其完全连接队列的值不一样,所以不想监听关闭该socket即可。
历史上backlog参数是设置这完全与非完全连接队列的大小,但是由于黑客的Syn攻击,导致非法连接占满了完全连接队列,导致正常客户的请求无法连接进来,所以为了防止SYN攻击,内核会作一些设置来防护,所以内核来设置半完全连接队列防止syn攻击,而具体接受多少连接数目由程序员来设定,所以backlog参数在之后就变为只是设置完全连接队列的大小的参数。
4.3.8 链接队列 半完全链接队列大小,可以通过系统参数 tcp_max_syn_backlog来决定默认32位下
512,64位下2048,最大8192。(测试环境是 Centos 6.5 32/64位) 完全链接队列大小,/proc/sys/net/core/somaxconn 来设置 , 最终大小为 1.5*backlog 和 内核参数俩个当中的最小值。
4.3. 9 int accept(int sockfd,struct sockaddr*addr,socklen_t *addr)
该函数是Tcp所使用的函数。该函数主要从上面所提到的完全链接队列中,提取完全链接队列头部的节点(pop ),如果完全链接队列为空则阻塞(默认是sockfd阻塞套接字)。 第一个参数 为listen态的套接字(必须是调用完bind 和 listen 的套接字) 第二个参数 为一个通用地址结构体,它主要用来返回远端 Client 的地址结构体(主要为输出型参数,直接定义直接传就行不用像bind还需相应的赋值) 第三个参数 为对应远端Client 的地址结构体大小。 如果不关心Client的信息,第二个与第三个设置为NULL。 对于服务器来讲,如果服务完这个Client后,需要把这个套接字close掉。
4.3.10 返回值
这个函数有三个返回值,第一个是int 它要么代表新连接的socket 或者 一个错误状态,第二个返回值是 远端Client的地址结构体,第三个参数是地址结构体的大小。
该结构体在 这个#include <netinet/in.h>头文件中
struct sockaddr_in { short sin_family;/*Address family一般来说AF_INET(地址族)PF_INET(协议族)*/ unsigned short sin_port;/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/ struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/ unsigned char sin_zero[8];/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/ }; typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; };4.3.11 终止连接函数 int close(int socketfd) 首先如果我们要正常终止连接的话就需要调用close函数,调用时只有对应的socketfd 对应的文件描述符对应的 file struct 结构体的引用计数为1的时候,调用才会触发正常的四次挥手否则只是引用计数减1. 如果TCP的发生缓冲区还有数据,调用close函数后立即返回,这时候内核会把发送缓冲区的数据发送出去但是并不会对这些数据进行ACK,随即就会发送FIN段进行连接终止。这样从应用层角度来看,我们是不知道对端主机是否收到了这些数据,可能这些数据对端并没有接收到对端主机就怠机了,接下来我端可能因为超时重传收到RST这些都是从kernel层的交互,我们应用层是不知道结果的。 其次TCP是双工的,调用close函数代表把该socket的读写都关闭,所以在发送了FIN段之后,如果对端 还发送数据,那么我端 会返回RST段去终止连接。 如果想要确保对端内核TCP层 收到这些数据,需要设置SO_LINGER选项,下面是不同设置了SO_LINGER选项调用 close 函数的结果。
struct linger { int l_onoff; 开关 int l_linger; 秒数 };1.l_onoff 设置为0 表明关闭该选项即进行默认close操作,调用后立即返回。 2.l_linger 设置为0,但是 l_onoff 非0,表明开启linger选项,但是立即终止连接,内核会丢弃所有TCP发生缓冲区的数据,并且使用异常的RST包来快速终止连接,对端 收到RST包后会立即关闭连接。(RST包 表示连接发生异常,列如当收到了一个非法序列号的包时就会发送RST异常终止连接,对端 收到RST会直接终止连接。) 这样设置可以避免主动关闭时进入的Time_wait状态,但缺点是快速建立相同的连接的时候即在2MSL内时,如果原链接上有旧的重发的包的话,则会导致新链接检验序列号是发现是非法序列号,导致新链接异常关闭。 3.俩个都非0 如果是阻塞套接字的话,close 就不会立即返回,这个时候会阻塞,除非有接下来俩个事情中的其中之一发生: 1.超时 2.发生缓冲区 的数据得到了ACK 如果是非阻塞套接字,上面俩个条件都未就绪返回EAGAIN。 最后无论套接字是否阻塞,在超时的时间内成功收到ACK,则close 返回0表成功,否则返回EWOULDBLOCK,然后发生缓冲区的数据会被丢弃,以RST的方式终止连接。
4.3.12 close函数在服务器上需要注意的点
1.多进程程序别忘了在fork之后,父进程关闭 accept的返回的socket,子进程别忘了关闭listensocket,否则会因为引用计数的原因无法正常触发终止序列。(FIN)
2.收到FIN段后需要即使调用close函数去关闭该socket,否则多路复用的机制(epoll)一直会提示该socket读就绪。
4.3.13 int shutdown( int sockfd , int howto) SHUT_WR 设置该选项后,如果发生缓冲区还有数据就发生出去,然后发生FIN段后函数返回,这个期间我们可以继续调用read函数读取数据,一直可以read到对端发生了FIN段,read函数返回0,这个时候从应用层的角度讲,我们的数据肯定已经被对端应用层接受了。 SHUT_RD 并不会发生FIN段,它只是抛弃接受缓冲区的数据,在调用完毕后,我们可以继续发生数据,期间对端有数据发送给我们,我们的内核tcp模块会帮我们ACK这些数据随即就扔了这些数据,上层应用是读不到的。 shutdown和close唯一的不同点在于,shutdown根本不关你引用计数多少都会关闭连接。
4.3.14 TIME_WAIT 通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。当主动关闭连接时,主动关闭方会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。(MSL:IP数据报能在互联网上最长的生命周期 ) 因为TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN。因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。(即若A端的FIN丢失了,要确保B端可以确认这个重发的FIN包而需发出最后这一个ACK的包,所以要有TMIE_WAIT的存在)。 因为TIME_WAIT的存在所以,客户端或服务器主动断开后不能立刻重启相应端口的服务。这对客服端没什么,但是对于服务器是致命的打击。假设一个大公司的服务器突然
断了,不能及时重连要进行TAME_WAIT等2倍的MSL的时间,这可能损失惨重。所以可以主动设置为断开连接时设置为RST方式,当关闭时发送RST段直接断开,不用进行TIME_WAIT.(当然接受方接收到RST会解释成一个错误),如何设置上面SO_LINGER的第二个场景有讲,还有一个方法避免Time_wait , 使用SO_REUSEADDR选项,下面列子中有。 SO_REUSEADDR选项不可以对listen socket使用,即使linstensocket使用SO_REUSEADDR,再启动一个进程对其进行重绑,会显示失败。
4.4 附属函数
4.4.1 ip数值与 点分ip字符串转换函数
in_addr_t inet_addr(const char *ip); 将点分四字节ip地址的字符串转为网络ip地址
char * inet_ntoa(struct in_addr in); 将网络ip地址转为点分四字节ip地址的字符串注意的是 inet_ntoa 是不可重人的函数(因为它把返回的结果保持在同一个静态的变量中)所以用多个临时变量去纪录 inet_ntoa的结果是不可取的,都指向同一个字符串,所以现在用inet_ntop去代替它。
4.4.2 主机序转换为网络序函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
这四个函数是用来将我们在代码内定义的数字转为网络号,因为从命令行拿到的端口号不能直接使用,或我们函数内部定义关于端口号的变量不能直接使用,需要转换后才能使 用。
4.4.3设置非阻塞套接字
if (fcntl(sfd, F_SETFL, fcntl(sfd, F_GETFL) | O_NONBLOCK) < 0) { perror("setting O_NONBLOCK"); close(sfd); break; }服务端:
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<string.h> #include <pthread.h> int startup(const char*ip,const char* port) { int sock=-1; if((sock=socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); exit(1); } int opt = 1; setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//设置端口复用,服务器主动断开不在time_wait struct sockaddr_in sock_addr; socklen_t len=sizeof(sock_addr); sock_addr.sin_family = AF_INET; sock_addr.sin_port= htons(atoi(port)); sock_addr.sin_addr.s_addr= inet_addr(ip); if((bind(sock,(const struct sockaddr * )&sock_addr,len))<0) { perror("bind"); exit(2); } if((listen(sock,0))<0){ perror("listen"); exit(3); } return sock; } void usesage(const char *p) { printf("correct : %s ip port",p); } void* Accept(void * arg) { int answer_sock=(int)arg; char buf[1024]; memset(buf,0,1024); ssize_t s; while(1) { if((s=read(answer_sock,buf,sizeof(buf)))>0) { buf[s]=0; printf("cilent %s",buf); printf("please Enter:"); fflush(stdout); s=read(0,buf,sizeof(buf)); answer_sock=(int)arg; write(answer_sock,buf,s); //这里大小为S特别重要,之前我一直写的是sizeof(buf),结果输出老出错 } else if(s==0) { printf(" cilent quit\n"); break; } else { perror("read"); pthread_exit((void*)1); } } close(answer_sock); return (void*)1; } int main(int argvs,const char*arg[]) { if(argvs!=3) { usesage(arg[0]); exit(4); } int listen=startup(arg[1],arg[2]); struct sockaddr_in server; socklen_t len=sizeof(struct sockaddr_in); while(1) { int answer_sock=accept(listen,(struct sockaddr *)&server,&len); printf(" yeah cilent success!\n"); pthread_t tid; pthread_create(&tid,NULL,Accept,(void*)answer_sock); pthread_detach(tid); } close(listen); return 0; }客户端:
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<stdlib.h> #include<string.h> int main(int argv,const char*arg[]) { int req_sock=socket(AF_INET,SOCK_STREAM,0); struct sockaddr_in sock_in; sock_in.sin_family=AF_INET; sock_in.sin_port= htons(atoi(arg[2])); sock_in.sin_addr.s_addr=inet_addr(arg[1]); socklen_t len=sizeof(sock_in); connect(req_sock,(struct sockaddr*)&sock_in,len); char buf[1024]; memset(buf,0,1024); while(1) { printf("please Enter:"); fflush(stdout); ssize_t s=read(0,buf,sizeof(buf)); buf[s]=0; s=write(req_sock,buf,s);//这里大小为S特别重要 if(s<0) { perror("write"); exit(1); } s=read(req_sock,buf,sizeof(buf)); if(s>0) { buf[s]=0; if(strcmp(buf,"quit")==0)break; printf("server say:%s",buf); fflush(stdout); } } return 0; close(req_sock); }验证结果如图4.1 4.2 4.3所示:
图4.1
图4.2
图4.3
基于TCP/IP协议和套接字编程原理为服务器端完成基本数据配置,主要采用套接字编程原理,通过调试运行并且经过完善后,实现并发服务器的功能。在这次课程设计的过程中,对于网络编程有了更深一步的了解。首先是对于计算机网络基础这方面,对于网络体系中的几种分层方法,每层的作用都由了系统的理解,特别是对于Socket编程这一块,套接字的创建、绑定以及使用,基于TCP、UDP的编程以及并发服务器的原理都有了深一步的理解,这次课程设计对于以后进行网络的开发有很大的帮助。
系统设计期间,学习到很多课堂上没有的知识,还积累了很多实践经验,增强了动手能力和解决实际问题的能力。通过这次的课程设计,对网络编程有了更深入的了解,进一步熟悉了TCP和UDP协议的内容,由于时间有限,该课程设计部分功能并未实现,例如客户端输入IP地址及端口号来进行连接的功能并未实现,希望在以后的学习生活中将这些功能意义实现,完成全部功能的实现。