socket编程
socket这个词可以表示很多概念,在TCP/IP协议中“IP地址 + TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP + 端口号”就称为socket。在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么两个socket组成的socket pair就唯一标识一个连接。
预备知识
网络字节序:内存中多字节数据相对于内存地址有大端小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,所以发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接收到的字节按内存从低到高的顺序保存,因此网络数据流的地址应该规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定网络数据流应该采用大端字节序,即低地址高字节。所以发送主机和接收主机是小段字节序的在发送和接收之前需要做字节序的转换。
为了使网络程序具有可移植性可以调用以下函数进行网络字节数的转换。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
1
2
3
4
5
6
7
socket地址数据类型及相关函数
sockaddr数据结构
IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有空指针类型这些函数的参数都⽤用struct sockaddr 类型表示,在传递参数之前要强制类型转换一下。
本次只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表⽰示32位的IP 地址。但是我们通常⽤用点分十进制的字符串表示IP 地址,以下函数可以在字符串表⽰示 和in_addr表⽰示之间转换。也就是说可以将字符串转换成in_addr类型,也可以将本地字节转换成网络字节。相反由同样有从网络转换到本地的函数具体的用法我们下面的代码中来看。
#include
#include
#include
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in)
1
2
3
4
5
6
7
8
9
10
11
12
13
tcpsocket 实现
实现模型:
1.服务器端 socket -> bind -> listen -> accept(阻塞,三次握手)-> send。
2.客户端 socket -> connect(阻塞,三次握手)-> rcv。
函数介绍:
int socket(int family, int type, int protocol)
1
family :指定协议的类型本次选择AF_INET(IPv4协议)。
type:网络数据类型,TCP是面向字节流的—SOCK_STREAM.
protocol:前两个参数一般确定了协议类型通常传0.
返回值:成功返回套接字符。
失败返回-1设置相关错误码。
int bind(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
1
sockfd : socket函数成功时候返回的套接字描述符。
servaddr :服务器的IP和端口。
addrlen : 长度(sizeof(servaddr))。
返回值:成功返回0
失败返回-1,并设置相关错误码。
int listen(int sockfd, int backlog)
1
sockfd: socket函数成功时候返回的套接字描述符。
backlog : 内核中套接字排队的最大个数。
返回值:成功返回0
失败返回-1,并设置相关错误码。
int accept(int sockfd, const struct sockaddr *servaddr, socklen_t *addrlen)
1
2
sockfd : socket函数成功时候返回的套接字描述符。
servaddr : 输出型参数,客户端的ip和端口。
addrlen : 长度(sizeof(servaddr))。
返回值:成功:从监听套接字返回已连接套接字
失败:失败返回-1,并设置相关错误码。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
1
sockfd:函数返回的套接字描述符
servaddr :服务器的IP和端口
addrlen : 长度(sizeof(servaddr))。
返回值:成功返回0
失败返回-1,并设置相关错误码
实现代码
server.c
#include
#include
#include
#include
#include
#include
#include
#include
static usage(const char* proc)
{
printf("Usage:%s[local-ip][local-port]\n",proc);
}
static int start_up(const char *local_ip,int local_port)
{
//1.create sock
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
close(sock);
exit(1);
}
//2,createbind
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(local_port);
local.sin_addr.s_addr = inet_addr(local_ip);
if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0)
{
perror("bind");
close(sock);
exit(2);
}
//3.listen
if(listen(sock,10) < 0)
{
perror("listen");
close(sock);
exit(3);
}
return sock;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
}
int sock = start_up(argv[1],atoi(argv[2]));
struct sockaddr_in client;
socklen_t len = sizeof(client);
while(1)
{
int new_sock = accept(sock,(struct sockaddr*)&client, &len);
if(new_sock < 0)
{
perror("accept");
close(sock);
return 1;
}
printf("client_ip:%s client_port:%d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
char buf[1024];
while(1)
{
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client say# %s",buf);
}
else if(s == 0)
{
printf("client quit!\n");
break;
}
write(new_sock,buf, strlen(buf));
}
}
close(sock);
return 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
client.c
#include
#include
#include
#include
#include
#include
#include
#include
static void usage(const char* proc)
{
printf("Usage:%s [server-ip] [server-port]",proc);
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 1;
}
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]);
if(connect(sock,(struct sockaddr *)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
while(1)
{
printf("please Entry#");
fflush(stdout);
char buf[1024];
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)//read success
{
buf[s] = 0;
}
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf)-1);
if(_s > 0)
{
buf[_s - 1] = 0;
printf("server echo# %s\n",buf);
}
}
close(sock);
return 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
上述代码只可以处理单个用户,为了可以处理多个用户请求我们可以编写多进程或者多线程的TCP套接字。完整代码如下。
多进程TCPsocket
https://coding.net/u/Hyacinth_Dy/p/MyCode/git/blob/master/%E5%A4%9A%E8%BF%9B%E7%A8%8BTCP%E5%A5%97%E6%8E%A5%E5%AD%97
多线程TCPsocket
https://coding.net/u/Hyacinth_Dy/p/MyCode/git/blob/master/%E5%A4%9A%E7%BA%BF%E7%A8%8BTcp%E5%A5%97%E6%8E%A5%E5%AD%97
对于上述代码就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即在性能上并不是合理的选择,因此我们需要提高代码的性能。下面介绍三种常用的高性能套接字编程方法。
I/O多路复用之select函数
select函数预备知识
struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作。
(1) FD_CLR(inr fd,fd_set* set):用来清除描述词组set中相关fd 的位
(2)FD_ISSET(int fd,fd_set *set):用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set):用来设置描述词组set中相关fd的位
2.struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。
FD_ZERO(fd_set *set);用来清除描述词组set的全部位
select函数介绍
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
1
maxfdp : 需要监视的最大文件描述符加1。
readfds、writefds、errorfds:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
timeout:等待时间,这个时间内,需要监视的描述符没有事件
发⽣生则函数返回,返回值为0。设为NULL 表示阻塞式等待,一直等到有事件就绪,函数才会返回,0表示非阻塞式等待,没有事件就立即返回,大于0表示等待的时间。
返回值:大于0表示就绪时间的个数,等于0表示timeout等待时间到了,小于0表示调用失败。
select函数原理
select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这⾥里等待,直到被监视的文件句柄有一个或多个发⽣生了状态改变。关于文件句柄,其实就是⼀一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。
1.我们通常需要额外定义一个数组来保存需要监视的文件描述符,并将其他没有保存描述符的位置初始化为一个特定值,一般为-1,这样方便我们遍历数组,判断对应的文件描述符是否发生了相应的事件。
2.采用上述的宏操作FD_SET(int fd,fd_set*set)遍历数组将关心的文件描述符设置到对应的事件集合里。并且每次调用之前都需要遍历数组,设置文件描述符。
3.调用select函数等待所关心的文件描述符。有文件描述符上的事件就绪后select函数返回,没有事件就绪的文件描述符在文件描述符集合中对应的位置会被置为0,这就是上述第二步的原因。
4.select 返回值大于0表示就绪的文件描述符的个数,0表示等待时间到了,小于0表示调用失败,因此我们可以遍历数组采用FD_ISSET(int fd,fd_set *set)判断哪个文件描述符上的事件就绪,然后执行相应的操作。
采用select的tcp socket实现代码。
#include
#include
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int array[4096];
static int start_up(const char* _ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(1);
}
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(2);
}
if(listen(sock,10) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return -1;
}
int listensock = start_up(argv[1],atoi(argv[2]));
int maxfd = 0;
fd_set rfds;
fd_set wfds;
array[0] = listensock;
int i = 1;
int array_size = sizeof(array)/sizeof(array[0]);
for(; i < array_size;i++)
{
array[i] = -1;
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0;i < array_size;++i)
{
if(array[i] > 0)
{
FD_SET(array[i],&rfds);
FD_SET(array[i],&wfds);
if(array[i] > maxfd)
{
maxfd = array[i];
}
}
}
switch(select(maxfd + 1,&rfds,&wfds,NULL,NULL))
{
case 0:
{
printf("timeout\n");
break;
}
case -1:
{
perror("select");
break;
}
default:
{
int j = 0;
for(; j < array_size; ++j)
{
if(j == 0 && FD_ISSET(array[j],&rfds))
{
//listensock happened read events
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listensock,(struct sockaddr*)&client,&len);
if(new_sock < 0)//accept failed
{
perror("accept");
continue;
}
else//accept success
{
printf("get a new client%s\n",inet_ntoa(client.sin_addr));
fflush(stdout);
int k = 1;
for(; k < array_size;++k)
{
if(array[k] < 0)
{
array[k] = new_sock;
if(new_sock > maxfd)
maxfd = new_sock;
break;
}
}
if(k == array_size)
{
close(new_sock);
}
}
}//j == 0
else if(j != 0 && FD_ISSET(array[j], &rfds))
{
//new_sock happend read events
char buf[1024];
ssize_t s = read(array[j],buf,sizeof(buf) - 1);
if(s > 0)//read success
{
buf[s] = 0;
printf("clientsay#%s\n",buf);
if(FD_ISSET(array[j],&wfds))
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\nyingying beautiful