然后和大家在会上讨论了才发现,其实大家都不会socket编程。上网一搜“socket编程”出现的大多就两个代码程序,一个服务器端用来接收N个字节,一个客户端用来连接服务器端并且发送N个字节。想必大多数人都是这么来学习和使用socket的吧。至此,开始记录下个人对socket编程在应用时的一些稍微深入些的知识。
当然,套接字有很多种,我们常用的是socket套接字,用于网络通信实现,最常用的也就是TCP/UDP的socket编程了。下面以Linux环境的TCP通信为例,说一说我所理解的socket编程。
首先觉得还是同样以那两个比较经典的程序来入门理解下socket编程。
服务器端:
……
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size;
int nbytes;
char buffer[1024];
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit(1);
}
bzero(&server_addr,sizeof(struct sockaddr_in));
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(portnumber);
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
exit(1);
}
if(listen(sockfd,5)==-1)
{
fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
exit(1);
}
while(1)
{
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
{
fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
exit(1);
}
fprintf(stderr,"Server get connection from %s\n",inet_ntoa(client_addr.sin_addr));
if((nbytes=read(new_fd,buffer,1024))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
exit(1);
}
buffer[nbytes]='\0';
printf("Server received %s\n",buffer);
close(new_fd);
}
close(sockfd);
exit(0);
}
从上段代码分析看来,其实服务器端实现的流程大概是:建立socket->填充socket需要的网络参数配置->绑定socket套接字->监听来自客户端的连接申请->接收客户端的连接申请,建立socket连接->读写数据。
下面我们在依次分析分析程序中用到的那些和socket有关的代码。
sockaddr_in结构体,它的作用是封装一些建立socket需要用到的参数,包括协议族、IP地址、端口号等,这些都和socket的建立息息相关。
再来看socket()这个函数,首先给出函数原型:
int socket(int domain, int type, int protocol);
第一个参数domain,协议族。本例中使用的是协议族是AF_INET是指ARPA因特网协议族,当然有了AF_INET6以后则表示IPV4协议族,而AF_INET6是指IPV6了。除此之外,还有很多在教材中不容易出现的协议族:AF_UNIX(有人说叫UNIX协议族,但其实确切的说,它指代的是文件系统协议族,从这里可以看出,原来socket不一定都是用来网络通信的),AF_ISO(顾名思义,ISO标准协议族),不同的Linux内核中支持不同的协议族,大家可以在头文件socket.h里面找到。
第二个参数type,类型。这里的类型指的是套接字类型,共分以下几种:流套接字SOCK_STREAM、数据报套接字SOCK_DGRAM、原始套接字SOCK_RAW、包套接字SOCK_PACKET等等。需要说明的是,这里的类型需和第一个参数的协议族对应起来,也就是说有些套接字是只能适用于某个或某些协议族的,好比以上四个套接字类型是适用在AF_INET这个协议族的。分别介绍下这四个套接字类型,很多人直接把SOCK_STREAM理解成TCP,SOCK_DGRAM理解成UDP,那是没问题的。而SOCK_RAW是一个IP层的套接字类型,我们在实现PING功能时就要用到这个套接字类型。SOCK_PACKET是一个链路层的套接字,相对底层一些,用的不多,但是如果你想实现一个用于ARP攻击的程序,它会帮助到你。
第三个参数protocol,协议。也就是具体用到的传输协议。例程中用的值是0,代表的是使用默认协议。那就需要解释一下系统怎么知道默认的协议是什么?答案来自于前两个参数,协议受限于协议族和类型。也就是说,大部分时候前两个参数定下来以后第三个参数可设置性就不那么强了,比如本例程中的,AF_INET和SOCK_STREAM设定后,就代表使用的是TCP协议了,就不能再把第三个参数又设成IPPOTO_UDP来使用UDP协议了。
返回值,如果执行成功则返回一个套接字描述符,例程中将它赋给了sockfd。如果失败的话则返回-1或INVALID_SOCKET。那这个描述符有什么用呢?首先个人理解的描述符就好比是C++下面的类指针。通俗的说,描述符代表了一个内存标识,我们通过这个描述符可以访问到一些相关的变量和函数。因为是内存中的标识,而且描述符是存放在进程的一个特殊列表中,所以它只能用于本进程,就算定义成全局变量也是一样的。
在执行socket函数后,建立了一个套接字并且得到了它的描述符sockfd,之后则是需要具体填充一个sockaddr_in,将协议族、地址、端口号都填充进去。这个sockaddr_in结构体有什么用呢?在于bind函数。
我们之前得到的sockfd描述符只是向操作系统、进程申请得到了部分空间,但并没有和实际的网络服务挂上钩。bind函数则是要将之前的套接字和实际的IP、端口、TCP网络层建立直接的联系。也就是说,在bind之前如果去访问sockfd描述符标识的部分参数或者调用部分函数,都没有任何的实际意义,并不会产生直接的网络动作。而bind之后,则会直接产生真实的网络动作了。
而之后的listen函数则是与与sockfd描述符相关联的函数,目的在于将该服务器socket挂出去接收客户端的连接申请。
对于服务器端,重点则是accept函数。可以看到,该函数通过之前的sockfd描述符得到了另一个新的new_fd描述符。而且在后续的read函数或者write函数所使用的描述符都是这个new_fd。这个如何理解?