单连接版本
掌握socket常见API(重要掌握)
1、创建socket文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain,int type,int protocol);
//参数1是指定协议类型(ipv4或者ipv6);参数2是指传输数据方式(面向数据报还是面向字节流);参数3默认为0;
2、绑定端口号(TCP/UDP,服务器)
int bind(int socket,const struct sockaddr* address,socklen_t address_len);
//参数1是socket文件描述符,参数2是绑定到本机的端口号和地址,参数3是参数2结构体的大小
3、监听socket(TCP,服务器)
只有监听成功才能允许客户端连接。
int listen(int socket,int backlog);//backlog相当于等待的服务器,只有多一点才有可能有空闲的处理请求
4、接收请求(TCP,服务器)
int accept(int socket,struct sockaddr* address,socklen_t* address_len);
5、建立连接(TCP,客户端)
int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
server.c:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<sys/socket.h>
4 #include<string.h>
5 #include<unistd.h>
6 #include<netinet/in.h>
7 #include<arpa/inet.h>
8
9
10 typedef struct sockaddr sockaddr;
11 typedef struct sockaddr_in sockaddr_in;
12
13 void ProcessConnect(int new_sock){
14 //完成本次连接的处理,循环的处理客户端的请求。
15 while(1){
16 char buf[1024]={0};
17 ssize_t read_size=read(new_sock,buf,sizeof(buf)-1);
18 if(read_size<0){
19 continue;
20 }
21 if(read_size==0){
22 //TCP中如果read的返回值为0,说明对端关闭了连接,因此再去读就只能读到0个字节
23 printf("[client %d]disconnect\n",new_sock);
24 close(new_sock);
25 return ;
26 }
27 buf[read_size]='\0';
28 printf("client[%d] say:%s\n",new_sock,buf);
29 //将相应结果写回客户端
30 write(new_sock,buf,strlen(buf));
31 }
32 }
33
34
35 int main(int argc,char* argv[]){
36 if(argc!=3){
37 printf("Usage:[ip]");
38 return -1;
39 }
40 //创建socket文件描述符
41 int sockfd=socket(AF_INET,SOCK_STREAM,0);
42 if(sockfd<0){
43 perror("socket\n");
44 return -2;
45 }
46 //绑定端口号
47 sockaddr_in server_addr;
48 server_addr.sin_family=AF_INET;
49 server_addr.sin_addr.s_addr=inet_addr(argv[1]);//inet_addr将点分十进制的字符串转成32位整数
50 server_addr.sin_port=htons(atoi(argv[2]));
51 if(bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr))<0){
52 perror("bind\n");
53 return 3;
54 }
55 //使用listen允许服务器被客户端连接
56 if(listen(sockfd,5)<0){
57 perror("listen");
58 return 1;
59 }
60 printf("bind and listen success,wait......\n");
61 //开始事件循环(TCP建立连接是在内核中建立的)
62 while(1){
63 sockaddr_in client;
64 socklen_t len=sizeof(client);
65 int req_sock=accept(sockfd,(sockaddr*)&client,&len);//将内核中建立好的连接放到用户空间(将小板凳上的人请到屋中)
66 if(req_sock<0){
67 perror("accept\n");
//不能因为单个客户端的失败挂掉整个服务器
68 continue;
69 }
70 ProcessConnect(req_sock);
71 }
72 return 0;
73 }
client.c
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<netinet/in.h>
6 #include<arpa/inet.h>
7 #include<string.h>
8 #include<sys/socket.h>
9
10
11 int main(int argc,char* argv[]){
12 if(argc!=3){
13 printf("Usage:[ip][port]\n");
14 return 1;
15 }
16
17 int sock=socket(AF_INET,SOCK_STREAM,0);
18 if(sock<0){
19 perror("sock\n");
20 return 1;
21 }
22 //指定客户端要连接的服务器ip和端口号是多少?
23 struct sockaddr_in server;
24 server.sin_family=AF_INET;
25 server.sin_addr.s_addr=inet_addr(argv[1]);
26 server.sin_port=htons(atoi(argv[2]));
27 int ret=connect(sock,(struct sockaddr*)&server,sizeof(server));
28 if(ret<0){
29 printf("connect failed\n");
30 return 1;
31 }
32 //事件循环
33 while(1){
34 char buf[1024]={0};
35 printf("client say:");
36 ssize_t read_size=read(0,buf,sizeof(buf)-1);
37 if(read_size<0){
38 perror("read fail\n");
39 return 1;
40 }
41 if(read_size==0){
42 printf("read done\n");
43 return 0;
44 }
45 buf[read_size]='\0';
46 write(sock,buf,strlen(buf));
47 printf("wait response..\n");
48 char buf_resp[1024]={0};
49 read_size=read(sock,buf_resp,sizeof(buf_resp)-1);
50 if(read_size<0){
51 perror("read fail\n");
52 return 1;
53 }
54 if(read_size==0){
55 printf("server close connect\n");
56 return 0;
57 }
58 buf_resp[read_size]='\0';
59 printf("server say:%s\n",buf_resp);
60 }
61 return 0;
62 }
由图中可以看出服务器处于监听状态。
运行程序可以实现服务器和客户端数据交互,而且当客户端退出时,服务器断开连接。
测试多个链接情况时,第二个客户端不能正常和服务器进行通信,原因在于我们设计accept一个请求之后就不断进入循环read(阻塞式),第一个客户端没有断开就不能调用到下一个accept,所以第二个连接一直在内核上(说明连接可以建立成功),没有在用户空间里所以就无法接受新请求。
所以就想到使用多进程版本使得accept之后引入多个执行流同时完成两类操作,并行执行互不干扰。(采用多进程版本和多线程版本)
多进程版本
具体思路是:创建进程让子进程再创建孙子进程处理请求,子进程退就能执行下一次accept。而父进程就负责防止僵尸进程的产生即可。
实现代码:
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6 #include<netinet/in.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<signal.h>
10
11
12 //进程中有一个致命的错误,创建子进程负责与客户端进行读写交互,fork同时会拷贝文件描述符表
13 //描述符表的生命周期随进程所以子进程后面调用exit(0);就会自动关闭sock文件描述符表,但是父进程不会,所以要手动关闭,以免泄漏。
14 void ProcessConnect(int newfd){
15 pid_t id=fork();
16 if(id<0){
17 perror("fork\n");
}
20 else if(id>0){
21 //father
22 //进程中有一个致命的错误,创建子进程负责与客户端进行读写交互,fork同时会拷贝文件描述符表\
23 // 描述符表的生命周期随进程所以子进程后面调用exit(0);就会自动关闭sock文件描述符表,\
24 // 但是父进程不会,所以要手动关闭,以免泄漏。
25 close(newfd);
26 return ;//让父进程负责不断调用accept。
27 }
28 //子进程只负责和客户端进行交互。如果read返回0,那么说明客户端关闭了文件描述符,\
29 //就让子进程直接退出,因为返回子进程就会执行accept(有父进程负责)
30 while(1){
31 char buf[1024]={0};
32 ssize_t read_size=read(newfd,buf,sizeof(buf)-1);
33 if(read_size<0){
34 continue;
35 }
if(read_size==0){
37 //说明对端关闭连接,那么子进程就直接退出即可
38 printf("[client %d] disconnect\n",newfd);
39 close(newfd);
40 exit(0);//不能使用return,这样的话子进程就会去执行accept,这是由父进程负责的。
41 }
42 buf[read_size]='\0';
43 printf("client say:%s\n",buf);
44 //服务器作出响应
45 write(newfd,buf,strlen(buf));
46 }
47 }
此时就可以多个客户端同时与服务器进行数据交互,但是创建进程虚拟地址空间需要各自独立一份故开销大,所以下面介绍多线程版本
多线程版本
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6 #include<netinet/in.h>
7 #include<sys/wait.h>
8 #include<string.h>
9 #include<pthread.h>
10
11 void* ThreadEntry(void* arg){
12 int64_t newfd=(int64_t)arg;
13 while(1){
14 char buf[1024]={0};
15 ssize_t read_size=read(newfd,buf,sizeof(buf)-1);
16 if(read_size<0){
17 continue;
18 }
19 if(read_size==0){
20
21 printf("[client %ld] disconnect\n",newfd);
22 close(newfd);
23 return NULL;
24 }
25 buf[read_size]='\0';
26 printf("client say:%s\n",buf);
27 //服务器作出响应
28 write(newfd,buf,strlen(buf));
29 }
30 return NULL;
31 }
32 void ProcessConnect(int64_t newsock){
33 pthread_t tid;
34 pthread_create(&tid,NULL,ThreadEntry,(void*)newsock);
35 //这里之所以可以强转是因为void* 有8个字节。超过int,但是用int64_t来表示解决警告。
36 pthread_detach(tid);
37 //为了保证线程能够回收而且可以很快的返回使用线程分离
38 }
39
40
41
42 int main(int argc,char* argv[]){
43 if(argc!=3){
44 perror("Usage[ip][port]\n");
45 return 1;
46 }
47 int sockfd=socket(AF_INET,SOCK_STREAM,0);
48 if(sockfd<0){
49 perror("socket\n");
50 return 2;
51 }
52 struct sockaddr_in addr;
53 addr.sin_family=AF_INET;
54 addr.sin_addr.s_addr=inet_addr(argv[1]);
55 addr.sin_port=htons(atoi(argv[2]));
56 int ret=bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
57 if(ret<0){
58 perror("bind\n");
59 return 1;
60 }
61 if(listen(sockfd,5)<0){
62 perror("listen\n");
63 return 2;
64 }
65 //客户端和服务器已经连接好,分流处理accept和处理请求。
66 while(1){
67 struct sockaddr_in client;
68 socklen_t len=sizeof(client);
69 int newfd=accept(sockfd,(struct sockaddr*)&client,&len);
70 if(newfd<0){
71 perror("accept\n");
72 continue;
73 }
74 printf("[client %d] connect\n",newfd);
75 ProcessConnect(newfd);
76 }
77 return 0;
78 }
比较多进程版本和多线程版本,多个客户端连接服务器,多进程版本是让依次退出了在连接上处理请求,所以socket文件描述符都是一样的,但是多线程版本是递增的,原因就在于多线程共用一份文件描述符表。