套接字实现Tcp服务器

来源:互联网 发布:淘宝退货率多少算正常 编辑:程序博客网 时间:2024/05/21 08:54
套接字编程又被叫做是socket编程,socket这个词可以表示很多概念: 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网

络通讯中的一个进程,“IP地址+端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socketpair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一 对一关系。

今天我们就在讲一下socket吧!

1、基本知识概念

1.1、网络字节序

我们都知道,内存中存储数据有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址、高字节(也就是数据的高位存在低地址)
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。。如果 主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

1.2、socket地址的数据类型

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同,如下图所示:

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些 控制字段。

1.3、实现套接字相关函数

【socket】创建套接字socket

       #include <sys/types.h>          /* See NOTES */       #include <sys/socket.h>       int socket(int domain, int type, int protocol);
参数domain表示的是建立的socket类型,当前的参数包含:

参数type表示的数据传输,是字节流传输,还是数据报传输,参数选择:

参数protocol表示创建的方式:在这默认缺省为  0 ;

【bind】服务器绑定函数bind

       #include <sys/types.h>          /* See NOTES */       #include <sys/socket.h>       int bind(int sockfd, const struct sockaddr *addr,                socklen_t addrlen);
此函数用来绑定服务器端的IP地址与端口号。
参数sockfd表示的是服务器的套接字——也就是socket函数生成的结果;
参数addr表示的是socket服务器的地址内容---内部用来写入服务器的ip与端口号;下面是一些对于IP地址操作的函数

参数addrlen表示的是传入的协议地址的大小;

【listen】设置套接字为监听状态

SYNOPSIS       #include <sys/types.h>          /* See NOTES */       #include <sys/socket.h>       int listen(int sockfd, int backlog);
此函数的作用是设置 sockfd套接字为监听状态,用来监听客户端的链接。
参数sockfd表示的是 要设置的套接字;
backlog表示的是服务器链接达到最大的数量之后,还可以放到等待队列的链接个数

【connect】链接函数一般用于客户端

SYNOPSIS       #include <sys/types.h>          /* See NOTES */       #include <sys/socket.h>       int connect(int sockfd, const struct sockaddr *addr,                   socklen_t addrlen);
此函数可以用于客户端连接到服务器
参数sockfd表示的表示的是要链接到服务器的客户端套接字;
参数addr表示的是服务器的地址与端口号;
参数addrlen表示的是addr的大小一般使用sizeof得到;

2、简单实现简洁版的tcp客户端服务器

实现代码:
服务器代码实现
#include<stdio.h>#include<stdlib.h>#include<sys/socket.h>#include<sys/types.h>#include<string.h>#include<netinet/in.h>static void  usage(){printf(".....:[ipaddr],[port]\n");exit(1);}//建立监听套接字int startup(char *  argv[]){//创建一个socketint sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd <0 ){perror("socket");exit(2);}//为网络协议地址赋值struct  sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port =  htons(atoi(argv[2]));addr.sin_addr.s_addr =  inet_addr(argv[1]);//将本地ip 与端口绑定到socket套接字上面if(bind(sockfd,(struct  sockaddr *)&addr,sizeof(addr)) <  0){perror("bind");exit(3);}//设置该套接字为监听套接字 且服务器接受链接达到最大时,可以保存的链接队列为10个if(listen(sockfd,10) <0){perror("listen");exit(4);}return sockfd;}int main(int argc,char  * argv[]){if(argc != 3){usage();}printf("server done\n");//得到监听套接字int sockfd = startup(argv);printf("%d\n",sockfd);while(1){struct sockaddr_in  addr;socklen_t addrlen =sizeof(addr);//使用accept来为服务器来拉链接  addr为输出型参数,保存 链接到的客户端地址int fd  = accept(sockfd,(struct  sockaddr*)&addr,&addrlen);if(fd < 0){continue;}//到这表示的找到链接 addr得到链接客户端的ip地址与端口号printf("get connent ip = %s,port = %d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));//之后双方通信char buf[1024];while(1){//服务器先读 后写//fd表示的是收到的客户端描述符int s = read(fd,buf,sizeof(buf)-1);//读到的字符个数为0 表示的是 读到的EOF表示读到文件的结尾if(s== 0){printf("client quit\n");close(fd);break;}else if(s< 0){perror("read");return 5;}else{buf[s] = '\0';printf("client #: %s\n",buf);//读完之后将读到的数据再次返回写给客户端write(fd,buf,strlen(buf));}}close(fd);}close(sockfd);return 0;}
客户端代码:
#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/socket.h>#include<string.h>#include<netinet/in.h>static void  usage(){printf(".......:[ipaddr],[port]\n");exit(1);}int main(int argc,char * argv[]){if(argc != 3){usage();}//建立套接字int sockfd = socket(AF_INET,SOCK_STREAM,0);if(socket < 0){perror("socket");return  2;}//客户端不需要来绑定ip地址与端口号,因为他是多对一的//设置协议地址 ip地址填服务器ip 端口号填服务器端口号struct sockaddr_in  addr;addr.sin_family =AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);//shiyongconnect函数链接到服务器if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0  ){perror("connent");return 3;}//链接成功之后else{printf("connect  success\n"); char  buf[1024]; while(1)   {//开始通信 服务器是先读后写,那么客户端就是先写后读了        printf("client #:");          fflush(stdout);//从标准输入中拿到数据           int s = read(0,buf,sizeof(buf)-1);            if(s <=0 )            {      perror("read");            return 4;    }     else      {           buf[s-1] = '\0';//写到sockfd套接字中            write(sockfd,buf,strlen(buf));         }//再从套接字中读取数据          s= read(sockfd,buf,sizeof(buf)-1) ;           if(s== 0)            {                 printf("server quit\n");                 break;            }            else if(s <0){       perror("read");         return 5;     }      else       {            buf[s] = '\0';              printf("server #:%s\n",buf);          }       }close(sockfd);    }     return  0; }
上面的代码只能实现一对一的服务器;我们要实现的服务器一般都是一对多的,下面我们实现多进程、多线程来实现服务器一对多通信。

3、使用多进程、多线程实现一对多通信的服务器代码

3.1、使用多进程实现

#include<stdio.h>#include<stdlib.h>#include<sys/socket.h>#include<sys/types.h>#include<string.h>#include<netinet/in.h>//实现多进程服务器static void  usage(){printf(".....:[ipaddr],[port]\n");exit(1);}//创建监听socketint startup(char *  argv[]){//创建一个socketint sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd <0 ){perror("socket");exit(2);}//为网络协议地址赋值struct  sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port =  htons(atoi(argv[2]));addr.sin_addr.s_addr =  inet_addr(argv[1]);//绑定服务器的IP地址与端口号if(bind(sockfd,(struct  sockaddr *)&addr,sizeof(addr)) <  0){perror("bind");exit(3);}if(listen(sockfd,10) <0){perror("listen");exit(4);}return sockfd;}int main(int argc,char  * argv[]){if(argc != 3){usage();}printf("server done\n");int sockfd = startup(argv);printf("%d\n",sockfd);while(1){struct sockaddr_in  addr;socklen_t addrlen =sizeof(addr);//accecpt来为服务器拉客户端int fd  = accept(sockfd,(struct  sockaddr*)&addr,&addrlen);//accept失败来继续accept;if(fd  < 0){continue;}//成功之后pid_t  pid = fork();//调用fork来实现多进程if(pid < 0 ){perror("fork");return 6;}//父进程在此执行读写else if(pid> 0){//parentprintf("get connent ip = %s,port = %d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));char buf[1024];while(1){int s = read(fd,buf,sizeof(buf)-1);if(s== 0){printf("client quit\n");close(fd);break;}else if(s< 0){perror("read");return 5;}else{buf[s] = '\0';printf("client #: %s\n",buf);write(fd,buf,strlen(buf));}}close(fd);waitpid(pid,NULL,0);}//子进程则继续来来拉客户端 else{continue;}}close(sockfd);return 0;}

3.2、使用多线程实现

#include<stdio.h>#include<stdlib.h>#include<sys/socket.h>#include<sys/types.h>#include<string.h>#include<pthread.h>#include<netinet/in.h>static void  usage(){printf(".....:[ipaddr],[port]\n");exit(1);}//创建监听socketint startup(char *  argv[]){//创建一个socketint sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd <0 ){perror("socket");exit(2);}//为网络协议地址赋值struct  sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port =  htons(atoi(argv[2]));addr.sin_addr.s_addr =  inet_addr(argv[1]);if(bind(sockfd,(struct  sockaddr *)&addr,sizeof(addr)) <  0){perror("bind");exit(3);}if(listen(sockfd,10) <0){perror("listen");exit(4);}return sockfd;}//为每个线程来执行一个客户端的读写void  * handler(void  * arg){int fd = (int)arg;char buf[1024];while(1){int s = read(fd,buf,sizeof(buf)-1);if(s== 0){printf("client quit\n");close(fd);break;}else if(s< 0){perror("read");return 5;}else{buf[s] = '\0';printf("client #: %s\n",buf);write(fd,buf,strlen(buf));}}close(fd);return NULL;}int main(int argc,char  * argv[]){if(argc != 3){usage();}printf("server done\n");int sockfd = startup(argv);printf("%d\n",sockfd);while(1){struct sockaddr_in  addr;socklen_t addrlen =sizeof(addr);//服务器accept来接受客户端发的链接int fd  = accept(sockfd,(struct  sockaddr*)&addr,&addrlen);if(fd <  0){continue;}//成功之后 创建新的线程来 执行与此客户端的读写操作。printf("get connent ip = %s,port = %d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));pthread_t  td;pthread_create(&td,NULL,handler,(void *)fd) ;//并且设置线程状态为 分离状态 这样主线程则不需要来等待分离线程,直接继续进行循环pthread_detach(td);}close(sockfd);return 0;}

4、TCP协议通讯流程

基于TCP协议的网络程序
下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器调用socket()、bind()、listen() 完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答⼀一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后 从accept()返回。
数据传输的过程: 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主 动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调 用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送 请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调 用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞 等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。