预备知识
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的端口号是为其他服务器预留的。
TCP套接字编程
服务器端源码:
#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执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。