一、socket编程
在理解TCP服务器时,我们必须了解socket编程,在上篇博客中,我们知道在TCP/IP协议中,“IP地址+TCP端口号/UDP端口号”唯一标识网络通讯中的唯一一个进程,我们把“IP地址+端口号”就成为socket。
在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socketpair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连接的一对一关系。
二、socket地址数据类型及相关函数
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址⽤sockaddr_in结构体表⽰,包括16位端口号和32位IP地址,IPv6地址⽤sockaddr_in6结构体表⽰,包括16位端口号、 128位IP地址和⼀些控制字段。
socket API是一层抽象的⽹网络编程接口,适⽤用于各种底层⽹网络协议,如IPv4、IPv6,以及UNIX Domain Socket。然⽽,各种网络协议的地址格式并不相同,如下图所示:
由上图可以看出:各种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 *类型表示,在传递参数之前要强制类型转换一下。
三、TCP通讯协议的基本流程
在实现TCP服务器之前,我们必须了解TCP通讯的基本过程,这样才能有一个比较清晰的思路实现我们的代码,基本流程图如下:
其基本过程如下: 服务器调⽤socket()、 bind()、 listen() 完成初始化后,调⽤accept()阻塞等待,处于监听端口的状态,客户端调⽤socket()初始化后,调⽤connect()发出SYN段并阻塞等待服务器应答,服务器应答⼀个SYN-ACK段,客户端收到后从connect()返回,同时应答⼀个ACK段,服务器收到后 从accept()返回。
四、服务器端与客户端通讯步骤
1、基于TCP的socket编程的服务器程序流程如下:
(1)创建套接字
(2)将套接字绑定到本地地址和端口上
(3)将创建的套接字设为监听模式,等待接收客户端的请求
(4)等待客户请求的到来,当请求到来后,接收连接请求,返回一个新的对于与此次连接的套接字
(5)用返回的套接字和客户端进行通信
(6).返回,等待另一个客户的请求
(7)关闭套接字
2、基于TCP的socket编程的客户度程序流程如下:
(1)创建套接字
(2)向服务器发出连接请求
(3)和服务器进行通信
(4)关闭套接字
客户端不需要绑定,如果绑定了,如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确连接。
五、客户端与服务器端程序(单进程)
1、客户端程序
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
#include<stdio.h> #include<unistd.h> #include<sys/socket.h> #include<sys/types.h> #include<string.h> #include<error.h> #include<errno.h> #include<netinet/in.h> #include<arpa/inet.h> int main(int argc,char*argv[]) { if(argc!=2) { printf("Usage:client IP\n"); return 1; } char *str=argv[1]; int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { printf("sock error\n"); return 2; } struct sockaddr_in server_sock; server_sock.sin_family=AF_INET; server_sock.sin_port=htons(8080); server_sock.sin_addr.s_addr=inet_addr(str); int ret=connect(sock,(const struct sockaddr*)&server_sock,sizeof(server_sock)); if(ret<0) { printf("connect failed...,error is:%d,errstring is:%s\n",errno,strerror(errno)); return 3; } printf("connect success...\n"); char buf[1024]; while(1) { memset(buf,0,sizeof(buf)); printf("client:#"); fflush(stdout); ssize_t s=read(0,buf,sizeof(buf)); buf[s-1]=0; if(strcmp(buf,"quit")==0) { printf("client quit!\n"); break; } write(sock,buf,sizeof(buf)); s=read(sock,buf,sizeof(buf)); if(s>0) { buf[s]=0; printf("server:%s\n",buf); } } close(sock); return 0; } |
2、服务器端程序
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <netinet/in.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #define _BACKLOG 10 int mysocket(char* IP,int port) { int listen_sock=socket(AF_INET,SOCK_STREAM,0); if(listen_sock<0) { printf("perror socket\n"); exit(2); } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=inet_addr(IP); int binding=bind(listen_sock,(struct sockaddr*)&local,\ sizeof(struct sockaddr_in)); if(binding<0) { printf("perror bind\n"); exit(3); } if(listen(listen_sock,_BACKLOG)<0) { printf("perror listen\n"); close(listen_sock); exit(4); } return listen_sock; } void service(int new_sock) { char buf[1024]; memset(buf,0,sizeof(buf)); while(1) { ssize_t s=read(new_sock,buf,sizeof(buf)); if(s<0) { printf("perror read\n"); exit(5); } else if(s==0) { close(new_sock); break; } buf[s]=0; printf("Client: %s\n",buf); write(new_sock,buf,strlen(buf)); } } int main(int argc,char*argv[]) { if(argc!=3) { printf("Usage:[IP],[port]!\n"); exit(1); } char* IP=argv[1]; int port=atoi(argv[2]); int listen_sock=mysocket(IP,port); printf("server:%d\n",listen_sock); struct sockaddr_in peer; socklen_t len=sizeof(peer); for(;;) { int new_sock=accept(listen_sock,(struct sockaddr*)&peer,&len); if(new_sock<0) { printf("perror accept\n"); continue; char bufip[32]; bufip[0]=0; inet_ntop(AF_INET,&peer.sin_addr,bufip,sizeof(bufip)); printf("get connect,ip is:%s port is:%d\n",\ bufip,ntohs(peer.sin_port)); service(new_sock); } close(listen_sock); return 0; } |
3、运行结果显示
要使程序成功通信,我们必须确保服务器端先运行,并且本机是开着的状态:
六、多进程服务器端版本
1、程序代码
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #include<sys/socket.h> #include<sys/wait.h> #include<netinet/in.h> #include<arpa/inet.h> void Usage() { printf("usage: ./server[ip][port]\n"); } void ProcessRequest(int client_fd,struct sockaddr_in*client_addr) { char buf[1024]={0}; for(;;) { ssize_t read_size=read(client_fd,buf,sizeof(buf)); if(read_size<0) { perror("read"); continue; } if(read_size==0) { printf("client:%s say bye!\n",inet_ntoa(client_addr->sin_addr)); close(client_fd); break; } buf[read_size]='\0'; printf("client %s say:%s\n",inet_ntoa(client_addr->sin_addr),buf); write(client_fd,buf,strlen(buf)); } return; } void CreateWorker(int client_fd,struct sockaddr_in*client_addr) { pid_t pid=fork(); if(pid<0) { perror("fork"); return; } else if(pid==0) { //child if(fork()==0) { ProcessRequest(client_fd,client_addr); } exit(0); } else{ //father close(client_fd); waitpid(pid,NULL,0); } } int main(int argc,char*argv[]) { if(argc!=3){ Usage(); return 1; } struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_addr.s_addr=inet_addr(argv[1]); addr.sin_port=htons(atoi(argv[2])); int fd=socket(AF_INET,SOCK_STREAM,0); if(fd<0) { perror("socket"); return 1; } int ret=bind(fd,(struct sockaddr*)&addr,sizeof(addr)); if(ret<0) { perror("bind"); return 1; } ret=listen(fd,10); if(ret<0) { perror("listen"); return 1; } for(;;){ struct sockaddr_in client_addr; socklen_t len=sizeof(client_addr); int client_fd=accept(fd,(struct sockaddr*)&client_addr,&len); if(client_fd<0) { perror("accept"); continue; } CreateWorker(client_fd,&client_addr); } return 0; } |
2、优缺点
优点:
(1)可以处理多个进程
(2)代码易于编写
(3)多进程服务器稳定,因为进程运行具有独立性
缺点:
(1)服务器性能比较差:因为只有在有新的连接进入时,系统才创建进程。
(2)占用资源较多,同时服务的连接有上限,并且上限很容易达到。
(3)耗费成本大,进程创建的越多,导致进程切换的周期越长,成本也变大,进而也影响了服务器的性能。