预备知识
socket(套接字):IP地址+端口号。可唯一标识网络通信中的一个进程。 单单只有端口号只能标识是这台主机上的唯一进程。那端口号都有哪些分类呢? 计算机端口可分为3大类: 1) 公认端口(Well Known Ports):从0到1023,它们紧密绑定于一些服务。通常这些端口的通讯明确表明了某种服 务的协议。例如:80端口实际上总是HTTP通讯。21端口是FTP服务。
2) 注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
3) 动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。 使用 TCP与UDP段结构中端口地址都是16比特,可以有在0—65535范围内的端口号。对于这65536个端口号有以下的使用规定: (1)端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1—1023之间的端口号,是由ICANN来管理的; (2)客户端只需保证该端口号在本机上是惟一的就可以了。客户端口号因存在时间很短暂又称临时端口号; (3)大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。
服务器端源码:
#include<stdio.h> #include<sys/types.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int startup(const char* ip,int port) //创建监听套接字 { int sock=socket(AF_INET,SOCK_STREAM,0);//建立服务器端socket if(sock<0) { perror("socket"); exit(1); } struct sockaddr_in server; server.sin_family=AF_INET; server.sin_addr.s_addr=inet_addr(ip); server.sin_port=htons(port); socklen_t len=sizeof(server); if(bind(sock,(struct sockaddr*)&server,len)<0)// 将套接字绑定到服务器的网络地址上 { perror("bind"); exit(2); } if(listen(sock,5)<0)// 建立监听队列 { perror("listen"); exit(3); } return sock; } int main(int argc,char* argv[]) { if(argc!=3) { printf("Use way:%s,[IP],[port]\n",argv[0]); return -1; } int listen_sock=startup(argv[1],atoi(argv[2])); struct sockaddr_in remote; socklen_t relen=sizeof(remote); while(1) //接收连接 { int ret=accept(listen_sock,(struct sockaddr*)&remote,&relen); // 等待客户端连接请求到达 if(ret<0) { perror("accept"); continue; } printf("client's ip is %s,port is %d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port)); char buf[1024]; while(1) { int s=read(ret,buf,sizeof(buf)-1); // 接收客户端数据 if(s>0) { buf[s]='\0'; printf("client#:%s\n",buf); } else { printf("client if quit!\n"); break; } } close(ret); } return 0; }客户端源码:
#include<stdio.h> #include<sys/types.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc,char* argv[]) { if(argc!=3) { printf("use way is %s,[ip],[port]\n",argv[0]); return -1; } int sock=socket(AF_INET,SOCK_STREAM,0);// 建立客户端socket if(sock<0) { perror("socket"); exit(1); } struct sockaddr_in server; // 服务器端网络地址结构 server.sin_family=AF_INET; server.sin_addr.s_addr=inet_addr(argv[1]); server.sin_port=htons(atoi(argv[2])); socklen_t len=sizeof(server); // 与远程服务器建立连接 if(connect(sock,(struct sockaddr*)&server,len)<0) { perror("connect"); exit(2); } char buf[1024]; while(1) { printf("send#:"); fflush(stdout); int s=read(0,buf,sizeof(buf)-1); if(s>0) { buf[s-1]='\0'; write(sock,buf,s); } } close(sock); return 0; }运行程序时,会发现一个现象,当先ctrl+c掉服务器进程时,再次启动服务器进程会出现bind: Address already in use的错误提示。 错误原因如下: bind 试图绑定一个已经在使用的端口。该陷阱也许没有活动的套接字存在,但仍然禁止绑定端口(bind 返回 EADDRINUSE),它由 TCP 套接字状态 TIME_WAIT 引起。该状态在套接字关闭后约保留 2 到 4 分钟(2MSL)。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。 那如果正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启,这种情况就比较麻烦了。但是是有解决方法的:给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。 服务器进程修改如下:
#include<stdio.h> #include<sys/types.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int startup(const char* ip,int port) //创建监听套阶字 { int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("socket"); exit(1); } struct sockaddr_in server; server.sin_family=AF_INET; server.sin_addr.s_addr=inet_addr(ip); server.sin_port=htons(port); socklen_t len=sizeof(server); int on=1; if((setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0) { perror("setsockopt"); exit(4); } if(bind(sock,(struct sockaddr*)&server,len)<0) { perror("bind"); exit(2); } if(listen(sock,5)<0) { perror("listen"); exit(3); } return sock; } int main(int argc,char* argv[]) { if(argc!=3) { printf("Use way:%s,[IP],[port]\n",argv[0]); return -1; } int listen_sock=startup(argv[1],atoi(argv[2])); struct sockaddr_in remote; socklen_t relen=sizeof(remote); while(1) //接收连接 { int ret=accept(listen_sock,(struct sockaddr*)&remote,&relen); if(ret<0) { perror("accept"); continue; } printf("client's ip is %s,port is %d\n",inet_ntoa(remote.sin_addr),ntohs(remote.sin_port)); char buf[1024]; while(1) { int s=read(ret,buf,sizeof(buf)-1); if(s>0) { buf[s]='\0'; printf("client#:%s\n",buf); } else { printf("client if quit!\n"); break; } } close(ret); } return 0; }此时再次运行程序,先ctrl+c掉服务器进程,然后再次运行不会再报错。
你也许会发现,服务器在创建套接字之后使用了bind函数将sockfd这个⽤于⽹络通讯的⽂件描述符监听myaddr所描述的地址和端口号绑定在一起。而客户端进程并没有bind。 这是因为,客户端通过调用connect函数在socket数据结构中保存本地和远端信息,无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。