socket编程之简单的TCP服务器

https://blog.csdn.net/leetcode8023/article/details/52132653

一、socket编程

    socket编程socket这个词可以表示很多概念: 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就称为socket。

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


二.端口号分类

(1).熟知端口号:数值在0~1024之间,由IANA把这些端口号分配给TCP/IP中最重要的一些应用程序,让所有的用户都能知道。(一般在服务器上绑定。)

(2).登记端口号:数值在1024~49151之间,这类端口号是为没有熟知端口号的应用程序使用的。使用这类端口号必须在IANA按照规定的手续登记,以防止重复。49151是65535的四分之三。(一般在服务器上使用)

(3).短暂端口号:数值在49152~65535之间,这类端口号是在客户程序运行时才动态选择,当通信结束时,这个刚才使用的端口号会被系统回收,以供其它客户进程使用。(在客户端使用)

三、网络字节序 

    我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式, 地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8, 也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的, 接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转 换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。


上边的函数其实很好记,h代表host,n代表net,htons则表示将host的端口号字节序转换为intet的字节序。


四、socket地址的数据类型及相关函数

    socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而各种网络协议的地址格式并不相同,如下图所示


    IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各 种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现 都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:





    sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址。但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示和in_addr表示之间转换。




TCP协议通讯流程在我的上一篇博客中详细的介绍了,本文不再赘述。


五、最简单的TCP网络应用程序

    server.c 的作用是接受client的请求,并与client进行简单的数据通信,整体为一个阻塞式的网络聊天工具。

1、socket函数


    socket()打开一个网络通讯端口,如果成功的话,就像open()那样返回一个文件描述符,应用程序可以像读写文件那样使用read/write在网络上收发数据,如果socket()调用出错则返回-1。

    对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表是面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协 议。protocol参数的介绍从略,指定为0即可。


2、bind函数

    服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。struct sockaddr *是一个通过指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,它们的长度各不相同,所以需要第三个参 数addrlen指定结构体的长度。



    设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址,这样设置可以在所有的IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底是哪个IP地址,端口号我们定义为8080。



3、listen函数

    典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog(宏)个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。


4、accept函数

    三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。成功返回一个新创建的scoket的描述符,不再需要监听该scoket,继续去接受scoket。

所以server的创建流程为:

1、创建 socket;

2、将socket 和 Server的IP、端口号进行绑定;

3、设置为监听状态;

4、每建立一个链接,就开启一个新的线程,在线程内部与对应的Client端进行通信

server.c

[cpp]  view plain  copy
  1. #include <stdio.h>                                                                                                                                    
  2. #include <sys/types.h>  
  3. #include <sys/socket.h>  
  4. #include <netinet/in.h>  
  5. #include <errno.h>  
  6. #include <string.h>  
  7. #include <unistd.h>  
  8. #include <signal.h>  
  9. #include <wait.h>  
  10. #include <pthread.h>  
  11.   
  12. void * thread_run(void * arg)  
  13. {  
  14.     char buf[1024];  
  15.     int fd = (int)arg;  
  16.     printf("create a new thread...\n");  
  17.         while(1)  
  18.         {  
  19.             ssize_t _s = read(fd,buf,sizeof(buf));  
  20.             if(_s > 0)  
  21.             {  
  22.                 buf[_s-1] ='\0';  
  23.             }  
  24.             else if(_s == 0)  
  25.             {  
  26.                 printf("client close...\n");  
  27.                 //close(fd);  
  28.                 break;  
  29.             }  
  30.             else  
  31.             {  
  32.                 break;  
  33.             }     
  34.             printf("client :%s\n",buf);  
  35.             write(fd,buf,strlen(buf));  
  36.         }  
  37.         close(fd);  
  38.         pthread_exit(0);  
  39. }  
  40.   
  41. int main()  
  42. {  
  43.                                                                                                                                                       
  44.     int sock = socket(AF_INET,SOCK_STREAM,0);  
  45.   
  46.  if(sock < 0)  
  47.     {  
  48.         perror("scok");  
  49.         return -1;  
  50.     }  
  51.   
  52.     struct sockaddr_in local;  
  53.     local.sin_family = AF_INET;  
  54.     local.sin_port = htons(8080);  
  55.     local.sin_addr.s_addr = inet_addr("192.168.234.129");  
  56.     //local.sin_addr.s_addr = htonl(INADDR_ANY);  
  57.     int opt = 1;  
  58.     setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));  
  59.     //bind  
  60.     if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)  
  61.     {  
  62.         perror("bind");  
  63.         close(sock);  
  64.         return 2;                                                                                                                                     
  65.     }  
  66.  //listen  
  67.     if(listen(sock,5)< 0)  
  68.     {  
  69.         perror("listen");  
  70.         close(sock);  
  71.         return 3;  
  72.     }  
  73.   
  74.     printf("bind and listen succes,wait accept..\n");  
  75.   
  76.     struct sockaddr_in client_sock;  
  77.     socklen_t len = sizeof(client_sock);  
  78.     while(1)  
  79.     {  
  80.         int new_fd  = accept(sock,(struct sockaddr*)&client_sock,&len);  
  81.         if(new_fd < 0)  
  82.         {  
  83.             perror("accept");  
  84.             close(sock);  
  85.             return 3;                                                                                                                                 
  86.         }  
  87.  <span style="white-space:pre">   </span>printf("get connection ,ip is :%s port is :%d\n",inet_ntoa(client_sock.sin_addr.s_addr),client_sock.sin_port);  
  88.         //create pthread  
  89.         pthread_t id;   
  90.         if(pthread_create(&id,NULL,thread_run,(void*)new_fd) < 0)  
  91.         {  
  92.             perror("phthread_create");  
  93.             close(sock);  
  94.             return 4;  
  95.         }  
  96.   
  97.         //线程分离  
  98.         pthread_detach(id);  
  99.     }  
  100.     close(sock);  
  101.     return 0;  
  102. }         


client.c的作用是链接server,并向server发起通信请求。

           由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意, 客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口号,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

connect函数

    客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对放的地址。connect()成功返回0,出错返回-1。


client创建过程

1、创建Socket;

2、连接;

3、通信。

[cpp]  view plain  copy
  1. #include <stdio.h>                                                                                                                                      
  2. #include <sys/types.h>  
  3. #include <sys/socket.h>  
  4. #include <netinet/in.h>  
  5. #include <errno.h>  
  6. #include <string.h>  
  7. #include <unistd.h>  
  8.   
  9. int main(int argc,char* argv[])  
  10. {  
  11.     if(argc !=3)  
  12.     {     
  13.         printf("Usage : server [IP] [PORT] \n");  
  14.         return 1;  
  15.     }     
  16.   
  17.     int sock = socket(AF_INET,SOCK_STREAM,0);  
  18.     if(sock < 0)  
  19.     {     
  20.         perror("scok");  
  21.         return -1;   
  22.     }     
  23.   
  24.     struct sockaddr_in server_sock;  
  25.     server_sock.sin_family = AF_INET;  
  26.     server_sock.sin_port = htons(atoi(argv[2]));  
  27.     server_sock.sin_addr.s_addr = inet_addr(argv[1]);  
  28.   
  29.     int ret = connect(sock,(struct sockaddr*)&server_sock,sizeof(server_sock));  
  30.     if(ret < 0)  
  31.     {     
  32.         perror("connect");  
  33.         close(sock);  
  34.         return 2;  
  35.     }  
  36.     printf("connect success...\n");  
  37.     char buf[1024];  
  38.     while(1)  
  39.         {  
  40.             printf("please enter :");  
  41.             fflush(stdout);  
  42.             ssize_t _s = read(0,buf,sizeof(buf));  
  43.             if(_s > 0)  
  44.             {  
  45.                 buf[_s] ='\0';  
  46.             }  
  47.             else if(_s == 0)  
  48.             {  
  49.    <span style="white-space:pre">     </span>close(sock);  
  50.                 break;  
  51.             }  
  52.             else  
  53.             {  
  54.                 break;  
  55.             }     
  56.             write(sock,buf,strlen(buf));  
  57.             printf("please wait...\n");  
  58.             read(sock,buf,sizeof(buf));  
  59.             printf("server # :%s",buf);  
  60.         }  
  61.     return 0;  
  62. }  

首先运行服务器查看监听状态


运行client端,给server端发送消息


server端显示效果



在接下来看一下这是怎么个情况??

现在做一个测试,首先启动server,然后启动client,然后使用Ctrl-C使server终止,这时马上再运行server,结果是:



        现在用Ctrl-C把client也终止掉,再观察现象:client终止时自动动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状 态。TCP协议规定,主动关闭连接的一端要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开connfd(192.168.234.129:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000), 虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()调用之间插入如下代码:

int opt = 1;

setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));


这次实现的简单的TCP服务器使用的是多线程,

多线程的优点:

无需跨进程边界; 

程序逻辑和控制方式简单; 

所有线程可以直接共享内存和变量等; 

线程方式消耗的总资源比进程方式好; 

多线程缺点:

每个线程与主程序共用地址空间,受限于2GB地址空间; 

线程之间的同步和加锁控制比较麻烦; 

一个线程的崩溃可能影响到整个程序的稳定性; 

到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数; 

线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU 

 

多进程优点:

每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

通过增加CPU,就可以容易扩充性能;

可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;

每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大

多进程缺点:

逻辑控制复杂,需要和主程序交互; 

需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 

多进程调度开销比较大; 

最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……

方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。



猜你喜欢

转载自blog.csdn.net/Rainie_Zhang_1/article/details/80369125