套接字编程

已经知道套接字(socket),可以在网络中利用其IP+port的特性,唯一标识一个网络中进程。所以,这次简单服务程序就运用套接字进行创建。以及对于这几个服务程序的优化。

在进行简单服务器创建之前,先了解几个关于套接字的知识:

  • 网络中的数据流采用大端字节序(数据的高位存放于存储单元的低地址中)。

  • 在网络中,发送数据之前,数据先从低地址发。

  • 为了在大端小端间进行网络字节序和主机字节序的转换,有以下几个库函数可供使用:

    1
    2
    3
    4
    5
    #include <arpa/inet.h>
    unit32_t htonl(unit32_t hostlong); // host to net's long type
    unit32_t htons(unit16_t hostshort); //host to net's short type
    unit32_t ntohl(unit32_t netlong); //net to host's long type
    unit32_t ntohs(unit16_t netshort); //net to host's short type

补充:

socket下的sockaddr三个结构体:

结构体名称 地址类型 其他
struct sockaddr 16b 14B的地址数据
sturct sockaddr_in 16b(AF_INET) 16b的端口号、32b的IP地址、8B的填充
struct sockaddr_un 16b(AF_UNIX) 108B路径名

其中,IPv4地址用sockaddr_in结构体表示,IPv6地址用sockaddr_in6结构体表示,包括16位的端口号和128位的IP地址和一些控制字段。UNIX Domain Socket则用sockaddr_un结构体表示。其中IPv4、IPv6和UNIX Domain Socket的地址类型分别定义为常数AF_INET、 AF_INET6、 AF_UNIX 。

TCP服务程序

TCP单进程服务程序

下面则是服务程序端的server.c:

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
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
//tell u the correct format
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int startup(const char *_ip,int _port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
if(listen(sock,5)<0){
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char *argv[])
{
//worng format
if(argc!=3)
{
usage(argv[0]);
return 1;
}
//start listen base on sever's socket
int listen_sock=startup(argv[1],atoi(argv[2]));
while(1)
{
//1.judge client is connected?
struct sockaddr_in client;
socklen_t len =sizeof(client);
int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_fd<0){
perror("accept");
continue;
}
//client's connect done...
printf("get a new client,%s->%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
while(1)
{
//start read from client..
char buf[1024];
ssize_t s=read(new_fd,buf,sizeof(buf)-1);
//read succeed...
if(s>0)
{
buf[s]=0;
printf("client: %s\n",buf);
write(new_fd,buf,strlen(buf));
}
//connect is break...
else{
printf("read done.....break!\n");
break;
}
}
close(new_fd);
}
return 0;
}

客户端程序,client.c:

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
//client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
static void usage(const char *proc)
{
printf("%s [server_ip] [server_port]\n",proc);
}
//it's client don't need the startup()
int main(int argc,char *argv[])
{
//input format is wrong...
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int sock =socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
perror("socket");
return 2;
}
struct sockaddr_in remote;
remote.sin_family=AF_INET;
remote.sin_port=htons(atoi(argv[2]));
remote.sin_addr.s_addr=inet_addr(argv[1]);
//connect sever..
if(connect(sock,(struct sockaddr*)&remote,sizeof(remote))<0){
perror("connect");
return 2;
}
//connect done..
while(1)
{
char buf[1024];
printf("plz input# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
//read is done,start write
if(s>0){
buf[s]=0;
write(sock,buf,strlen(buf));
ssize_t _s=read(sock,buf,sizeof(buf)-1);
if(_s>0){
buf[_s]=0;
printf("server echo# %s\n",buf);
}
}
}
close(sock);
return 0;
}

测试时,记得关闭防火墙service iptable stop

TCP多进程服务程序

上面的server.c,虽然按我们当初设想实现了client与server简单的通信,但是可以发现,仅仅是对于一个client的server,并不能处理多个请求与server通信的情况。所以自然而然想到,用fork()创建子进程的方法,使server进而支持多个client的通信请求。

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 <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int startup(const char *_ip,int _port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
if(listen(sock,5)<0){
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));
while(1)
{
struct sockaddr_in client;
socklen_t len =sizeof(client);
int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_fd<0){
perror("accept");
continue;
}
printf("get a new client,%s->%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//just fork ...
pid_t id=fork();
if(id<0){
perror("fork");
close(new_fd);
}
//fork succeed..
else if(id==0){//child
close(listen_sock);
//when while in child,one case is child run ,but father is done,make child be a zombine..
//so read and write in grandson..
if(fork()>0)
{
exit(0);
}
//grandson fork done..child exit,so father exit too..
//read and write base on grandson..
while(1)
{
char buf[1024];
ssize_t s=read(new_fd,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("client: %s\n",buf);
write(new_fd,buf,strlen(buf));
}
else{
printf("read done.....break!\n");
break;
}
}
close(new_fd);
exit(1);
}
else{//father
close(new_fd);
waitpid(id,NULL,0);
}
}
return 0;
}

TCP多线程服务程序

有多进程解决多个client请求通信,肯定也可以用多线程解决。**

注意:创建线程,Makefile中gcc后应该加选项-lpthread

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
//server.c
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int startup(const char *_ip,int _port)
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(3);
}
if(listen(sock,5)<0){
perror("listen");
exit(4);
}
return sock;
}
void* request(void *arg)
{
int new_sock=(int)arg;
while(1)
{
char buf[1024];
ssize_t s=read(new_sock,buf,sizeof(buf)-1);
if(s>0)
{
buf[s]=0;
printf("client: %s\n",buf);
write(new_sock,buf,strlen(buf));
}
else{
printf("read done.....break!\n");
break;
}
}
close(new_sock);
return (void*)0;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int listen_sock=startup(argv[1],atoi(argv[2]));
while(1)
{
struct sockaddr_in client;
socklen_t len =sizeof(client);
int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_fd<0){
perror("accept");
continue;
}
printf("get a new client,%s->%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pthread_t id;
//create a pthread,and run request to read and write..
pthread_create(&id,NULL,request,(void*)new_fd);
pthread_detach(id); //detach..so don't need to join..
}
return 0;
}

UDP服务程序

写了基于以上tcp_server的UDP单进程sever.c,UDP不同于TCP,只管发送,所以在server.c中bind后直接recvfrom client,在sendto client以作echo。

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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 3;
}
char buf[1024];
struct sockaddr_in client;
while(1)
{
socklen_t len=sizeof(client);
ssize_t s=recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);
if(s>0)
{
buf[s]=0;
printf("[%s:%d] %s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));
}
}
close(sock);
return 0;
}

client.c

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
//client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
static void usage(const char *proc)
{
printf("%s [server_ip] [server_port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
usage(argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
char buf[1024];
//peer is sever..
struct sockaddr_in peer;
while(1)
{
socklen_t len=sizeof(peer);
printf("plz input# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s-1]=0;
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
ssize_t _s=recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
if(_s>0){
buf[_s]=0;
printf("server echo# %s\n",buf);
}
}
}
close(sock);
return 0;
}

思考

以上服务程序的逻辑很简单,但也算是实现了我们需要的通信的功能。第一、在TCP的服务程序中,最后改变成为多进程或多线程的服务程序,以求来进行多client程序的响应。但是,对于这三个服务程序来说,直接在sever服务程序中进行fork(),或是pthread的create,对于现实中资源有限的服务器,代价未免过大。。所以这是缺点之一;第二、在client程序中有连接时才进行进程或线程的创建,这样在第一个条件的限制下,访问量巨大时,服务器的性能必然直线下降,带来的创建过程的时间消耗。

所以针对以上,就提出了线程池与进程池的概念:

线程池&进程池

首先,了解关于池的概念,由于服务器硬件资源的相对“富足”,以空间换时间的思想,“浪费”一部分资源,来换取更高的运行效率,这就是池。另外,在服务器运行之初,就已创建并被初始化,成为静态资源分配。

那么线程池/进程池,则就是服务器预先创建的一部分子线程/子进程,其中子进程的数目在3~10个,子线程的数目则应与CPU数量差不多。

而对于一个线程/进程池来说应该有一下几部分:

  • 线程/进程管理器:用于创建并管理线程/进程池。
  • 工作线程/进程:线程/进程池中实际执行任务的线程/进程。
  • 任务接口:每个任务必须实现的接口:当线程池的任务队列中有可执行任务时,被空闲的工作线程调去执行(其中线程的闲忙是通过互斥量实现的)。
  • 任务队列:用来存放没有处理的任务。(通常用先进先出的原理,也可以用链表)。

主进程使用来选择子进程的算法,最简单的有随机算法和Round-Robin(轮流选取)算法,显而易见,更优秀的算法,更大程度上可以减轻服务器的压力。

主进程和子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上,等待主进程的唤醒。