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