socket

[toc]

基础知识

socket常用函数

socket

domain指定协议族,type指定socket类型,protocol指定协议(为0时自动选择type类型对应的默认协议)

1
int socket(int domain, int type, int protocol);

返回值大于0成功;-1失败,错误类型保存在全局变量errno中。

bind

分配地址族中的特地地址给socket
sockfd为socket描述字,通过socket函数的返回值确定
addr指向绑定给sockfd的地址,根据socket创建时协议族的不同而不同。

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值等于0成功;-1失败,错误类型保存在全局变量errno中。

sockaddr*可能的若干结构

ipv4
1
2
3
4
5
6
7
8
9
10
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};

struct in_addr {
uint32_t s_addr;
};

ipv6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sockaddr_in6 { 
sa_family_t sin6_fami一个ly;
in_port_thtonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short" sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};

struct in6_addr {
unsigned char s6_addr[16];
};

listen

服务器监听指定的套接字,backlog指定socket最大连接个数。

1
int listen(int sockfd, int backlog);

返回值等于0成功;-1失败,错误类型保存在全局变量errno中。

connect

客户端调用connect函数与服务器进行连接。

sockfd为客户机的套接字描述符,addr为服务器的套接字地址,addrlen为socket地址的长度。

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户机一般不指定自己的端口,系统在1024-5000中选一个(即自由端口),5000以上作为公共端口。

accept

当服务器listen到某htonl()—“Host to Network Long”
ntohl()—“Network to Host Long”
htons()—“Host to Network Short”
ntohs()—“Network to Host Short”客户机的connect连接请求,则可以通过accpet函数接受请求,同时建立一个新的套接字,专门用于本次的通信服务。

sockfd为服务器的监听socket描述符,addr为其地址,addrlen为协议地址的长度,返回连接套接字的描述字。

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

IO

服务器与客户机像操作文件一样操作socket进行通信。

有许多对IO操作:

  • read/write
  • recv/send
  • readv/writev
  • recvmsg/sendmsg
  • recvfrome/sendto

暂时仅考虑一对IO操作(read/write)

1
2
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

close

将套接字引用计数-1

close后进程不再能访问该套接字,但套接字的清除由TCP延后完成。

1
int close(int sockfd);

getsockname, getpeername

获取套接字的名字。

1
2
int getsockname(int fd,struct sockaddr* localaddr,int* addrlen);
int getpeername(int fd,struct sockaddr* peeraddr,int* addrlen);

字节顺序转换

htonl()—“Host to Network Long”
ntohl()—“Network to Host Long”
htons()—“Host to Network Short”
ntohs()—“Network to Host Short”

参考博客

Socket原理讲解

Unit 02

简单的交互示例

client向server发送三条消息hello hello1 hello2

server收到消息后回复一个welcome

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>

#define MAXDATASIZE 128
#define PORT 3000
#define BACKLOG 5

int main(int argc,char **argv){
int sockfd, new_fd, nbytes, sin_size;
char buf[MAXDATASIZE];
struct sockaddr_in srvaddr, clientaddr;

//1.创建网络端点
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("can;t create socket\n");
exit(1);
}

//套接字选项 增添端口地址可重用选项,使得某台服务器崩溃后可以快速用其余服务器重新提供服务
if(argc==2){
int on=1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
printf("reuse addr\n");
}

//填充地址
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
//htons进行字节顺序转换
srvaddr.sin_port=htons(PORT);
//服务器上无论有多少个IP发来消息,全部都予以接受
srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);

//将xxx.xxx.xxx.xxx转换为32位的地址
/*
if(inet_aton(argv[1],&srvaddr.sin_addr)==-1){
printf("addr convert error\n");
exit(1);
}
*/
//2.绑定服务器地址和端口
if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("bind error\n");
exit(1);
}
//3. 监听端口,将主动套接字转换为被动套接字
if(listen(sockfd,BACKLOG)==-1){
printf("listen error\n");
exit(1);
}
//一般应该在循环中添加一些退出的方法
for(;;){
//4.接受客户端连接
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr, (socklen_t*)&sin_size))==-1){
printf("accept error\n");
continue;
}
//如果成功连接,那么打印出客户机的ip、port
printf("client addr:%s %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

//5.接收请求
//延时 必须读入一个字符才继续 可以检查读入的客户机数据的情况(但是很麻烦所以我删了)
//getchar();

//sockfd属于监听套接字,new_fd属于连接套接字
nbytes=read(new_fd,buf,MAXDATASIZE);
buf[nbytes]='\0';
printf("client:%s\n\n",buf);

//6.回送响应
sprintf(buf,"wellcome!");
write(new_fd,buf,strlen(buf));

//关闭socket
close(new_fd);
}
close(sockfd);

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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define PORT 3000

int addr_conv(char *address,struct in_addr *inaddr);

int main(int argc,char **argv){
int sockfd,nbytes;
int port=PORT;
char buf[MAXDATASIZE];
struct sockaddr_in srvaddr;

//必须输入1/2个参数,具体格式参见printf的内容
if(argc!=2 && argc!=3){
printf("usage:./client hostname|ip. Or usage:./client hostname|ip port\n");
exit(0);
}
//如果有指定端口号则使用,不然使用默认端口PORT=3000
if(argc==3) port=atoi(argv[2]);

//1.创建网络端点
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("can;t create socket\n");
exit(1);
}
//指定服务器协议簇,端口(本地socket地址采用默认值)
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(port);
/*
if(inet_aton("127.0.0.1",&srvaddr.sin_addr)==-1){
printf("addr convert error\n");
exit(1);
}
*/
//根据传入参数指定服务器IP地址
if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
perror(strerror(errno));
}
//2.连接服务器
//客户端的IP地址等信息不需要手动填写,由内核自动完成
if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("connect error\n");
exit(1);
}

//3.发送请求
sprintf(buf,"hello"); write(sockfd,buf,strlen(buf));
sprintf(buf,"hello2"); write(sockfd,buf,strlen(buf));
sprintf(buf,"hello3"); write(sockfd,buf,strlen(buf));

//4.接收响应
if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
printf("read error\n");
exit(1);
}
buf[nbytes]='\0';
printf("srv respons:%s\n\n",buf);

//关闭socket
close(sockfd);
return 0;
}

int addr_conv(char *address,struct in_addr *inaddr){
struct hostent *he;
//inet_aton 函数将一个“字符串类型”的IP地址转化为“整数类型”的地址
if(inet_aton(address,inaddr)==1){
printf("call inet_aton sucess.\n");
return 0;
}
printf("call inet_aton fail.\n");
//如果输入的不是IP地址而主机名/域名,那么可以通过这些来获得IP地址
he=gethostbyname(address);
if(he!=NULL){
printf("call gethostbyname sucess.\n");
*inaddr=*((struct in_addr *)(he->h_addr_list[0]));
return 0;
}
return -1;
}

套接字最大创建数量

client_msock.c(主动套接字最大数量)

检查最多可创建的套接字数量
向程序传入两个参数,第一个参数为IP地址,第二个参数为想要创建的套接字数量
具体实现其实是On地尝试创建所有希望的套接字,出错则退出。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define PORT 3000

int addr_conv(char *address,struct in_addr *inaddr);


int main(int argc,char **argv){
int i,sockfd;
struct sockaddr_in srvaddr;
int n=atoi(argv[2]);

//1.创建网络端点
for(i=0;i<n;i++)
{
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("Failed to create the %d socket.\n",i);
exit(1);
}
//指定服务器地址(本地socket地址采用默认值)
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(PORT);

if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
perror(strerror(errno));
}
}

printf("Finished.\n");
return 0;
}

server_msock.c(被动套接字最大数量)

传入2/4个参数,表示起始端口号、创建连续套接字数量、发送信息大小、接受信息大小。

具体实现与上述类似,暴力模拟,检验是否可行。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>

#define MAXDATASIZE 128
#define BACKLOG 5

int main(int argc,char **argv){
int sockfd,new_fd,nbytes,sin_size;
char buf[MAXDATASIZE];
struct sockaddr_in srvaddr;
int i,lastsockfd=-1;
int minport,n,sizeSND,sizeRCV;

if(argc!=3 && argc!=5)
{
printf("You should input 3 or 5 params: ");
printf("func, startPort, number, SND size, RCV size.\n");
exit(1);
}

minport=atoi(argv[1]);
n=atoi(argv[2]);
if(argc==5)
{
sizeSND=atoi(argv[3]);
sizeRCV=atoi(argv[4]);
}

for(i=0;i<n;i++)
{
//1.创建网络端点
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("Failed to create %d socket.The last sockfd is %d.\n",i,lastsockfd);
exit(1);
}
lastsockfd=sockfd;

if(argc==5){ // get & set SO_SNDBUF,SO_RCVBUF
int size;
socklen_t size2=sizeof(size);
if((getsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&size,&size2))<0)
{
printf("getsockopt failed\n");
exit(1);
}
//printf("SND buff is %d\n",size);
if((getsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,&size2))<0)
{
printf("getsockopt failed\n");
exit(1);
}
//printf("RCV buff is %d\n",size);

if((setsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&sizeSND,sizeof(sizeSND)))<0
|| (setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&sizeRCV,sizeof(sizeRCV)))<0)
{
printf("setsockopt failed.\n");
exit(1);
}
// printf("New SND and RCV buff are %d and %d.\n",sizeSND,sizeRCV);
// exit(0);
}

//填充地址
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(minport+i);
srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);

//2.绑定服务器地址和端口
if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("bind error\n");
exit(1);
}
//3. 监听端口
if(listen(sockfd,BACKLOG)==-1){
printf("listen error\n");
exit(1);
}
}

printf("Finished.The last sockfd is %d.\n",sockfd);
return 0;
}

统计自由端口数量

具体实现是客户端尝试进行充分多次的套接字连接请求,服务器端每次根据发送方的IP地址更新最大最小值,这样就能知道自由端口所在的区间。.

大概自由端口保证是连续的

client_port.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define PORT 3000

int addr_conv(char *address,struct in_addr *inaddr);

int main(int argc,char **argv){
int sockfd,nbytes,i,j;
char buf[MAXDATASIZE];
struct sockaddr_in srvaddr;
if(argc!=2){
printf("usage:./client hostname|ip\n");
exit(0);
}

bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(PORT);
if(addr_conv(argv[1],&srvaddr.sin_addr)==-1)
{
perror(strerror(errno));
exit(1);
}

for(j=0;j<100;j++)
{
for(i=0;i<10000;i++)
{
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("can;t create socket\n");
exit(1);
}
if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("connect error\n");
exit(1);
}

sprintf(buf,"hello");
write(sockfd,buf,strlen(buf));
if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
printf("read error\n");
exit(1);
}
close(sockfd);
}

printf("srv respons 10k times:%d\n",j);
}

return 0;
}

server_port.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>

#define MAXDATASIZE 128
#define PORT 3000
#define BACKLOG 5

int main(int argc,char **argv){
int sockfd, new_fd, nbytes, sin_size;
unsigned int port, minport, maxport;
char buf[MAXDATASIZE];
struct sockaddr_in srvaddr,clientaddr;

//1.创建网络端点
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
printf("can;t create socket\n");
exit(1);
}

if(argc==2){
int on=1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
printf("reuse addr\n");
}
//填充地址
bzero(&srvaddr,sizeof(srvaddr));
srvaddr.sin_family=AF_INET;
srvaddr.sin_port=htons(PORT);
srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);

//2.绑定服务器地址和端口
if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
printf("bind error\n");
exit(1);
}
//3. 监听端口
if(listen(sockfd,BACKLOG)==-1){
printf("listen error\n");
exit(1);
}在执行 make 之

minport=65535;
maxport=0;
printf("Listening......\n");
for(;;){
//4.接受客户端连接
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr,&sin_size))==-1){
printf("accept error\n");
continue;
}

port=(unsigned int)ntohs(clientaddr.sin_port);
if(port<minport || port>maxport)
{
if(port<minport)
minport=port;
else if(port>maxport)
maxport=port;

printf("minport is: %d\t",minport);
printf("maxport is: %d\n",maxport);
}
nbytes=read(new_fd,buf,MAXDATASIZE);
write(new_fd,buf,strlen(buf));
close(new_fd);
}
close(sockfd);

return 0;
}

:star: 客户端发送整数,服务器计算平均值后返回

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
#include <termio.h>
#include <pthread.h>
#include <stdarg.h>

#define itn int
#define whlie while
#define fro for
#define pritnf printf

struct termios tmtemp, tm;
char getch(void){
int c = 0, fd = 0;

//获取当前的终端属性设置,并保存到tm结构体中
if(tcgetattr(fd,&tm) != 0) return -1;

tmtemp=tm;

//将tetemp初始化为终端原始模式的属性设置
cfmakeraw(&tmtemp);

//终端设置为原始模式的设置
if(tcsetattr(fd,TCSANOW,&tmtemp) != 0) return -1;

c=getchar();

if(tcsetattr(fd,TCSANOW,&tm) != 0) return -1;

return (char)c;
}

int myprintf(char *format, ...)
{
struct termios now;
if(tcgetattr(0, &now) != 0) return -1;
if(tcsetattr(0,TCSANOW,&tm) != 0) return -1;

va_list args;
va_start(args, format);
int cnt = vprintf(format, args);
va_end(args);

if(tcsetattr(0,TCSANOW,&now) != 0) return -1;
return cnt;
}

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
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>

#define SERVER_PORT 8086

int n, buf[1024], msg[1024];

int fun(char *ip){
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){
fprintf(stderr, "Socket error\n");
return -1;
}

bzero(&servaddr, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
if(inet_aton(ip, &servaddr.sin_addr) == -1){
fprintf(stderr,"Inet_aton error\n");
return -1;
}

if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0){
fprintf(stderr,"Connect error\n");
close(sockfd);
return -1;
}

msg[0] = htonl(n);
for(int i = 0; i < n; i++) msg[i + 1] = htonl(buf[i]); msg[n + 1] = 0;

write(sockfd, msg, (n + 1) * sizeof(int));

n = (int)read(sockfd, msg, 1024);
if(n < 0){
fprintf(stderr, "Read error\n");
return -1;
}
printf("服务器返回平均值为%d\n", ntohl(msg[0]));
close(sockfd);
return 0;
}

int main(int argc, char *argv[]){
if(argc != 2){
puts("请输入一个参数表示服务器IP地址!");
return 0;
}

srand((int)time(NULL));

n = 255;
for(int i = 0; i < n; i++) buf[i] = rand() % 100;
printf("总共产生%d个数字:\n", n);
for(int i = 0; i < n; i++) printf("%d ", buf[i]); puts("");

fun(argv[1]);

return 0;
}

server

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
105
106
107
108
109
110
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <time.h>

#include "rqdmap.h"


#define SERVER_PORT 8086
#define BACKLOG 5

char op;
void * thread(void * listenfd){
while(op != 'q'){
fflush(stdin);
op = getch();
printf("receive %d\n", op);
}
close(*(int *)listenfd);
exit(1);
}

int buf[1030];


int fun(int connfd){
int n = (int)read(connfd, (char*)buf, 1100);
if(n < 0){
fprintf(stderr,"Read error");
return -1;
}
n = ntohl(buf[0]);
int sum = 0;

// for(int i = 0; i < n; i++) printf("%d ", ntohl(buf[i + 1]));
for(int i = 1; i <= n; i++) sum += ntohl(buf[i]);
sum /= n;

if(myprintf("服务器输出 %d \n", sum) < 0){
fprintf(stderr, "Print error\n");
return -1;
}

buf[0] = htonl(sum); buf[1] = 0;
write(connfd, buf, sizeof(int));
return 0;
}


int main(){
int listenfd, connfd;
struct sockaddr_in servaddr;

listenfd = socket(AF_INET, SOCK_STREAM, 0);
printf("产生套接字 %d\n", listenfd);
if(listenfd < 0){
fprintf(stderr, "Socket error\n");
exit(1);
}

int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

bzero(&servaddr, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port=htons(SERVER_PORT);
if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof (struct sockaddr)) < 0){
fprintf(stderr,"Bind error\n");
exit(1);
}

if(listen(listenfd, 5) < 0){
fprintf(stderr,"Listen error\n");
close(listenfd);
exit(1);
}


pthread_t pid;
if(pthread_create(&pid, NULL, thread, &listenfd) != 0){
puts("Create pthread error!");
exit(1);
}

while(1){
connfd = accept(listenfd, NULL, NULL);
if(connfd < 0){
fprintf(stderr,"Accept error\n");
exit(1);
}

if(fun(connfd) != 0){
fprintf(stderr, "fun error\n");
op = 'q'; break;
}
close(connfd);
}

pthread_join(pid, NULL);
return 0;
}

实战记录

遇到了非常多的坑点…弄了很久

利用socket套接字进行数据通信最关键的地方其实在于 通信格式的设计,这样接收方才能够正确无误地解读发送方的比特串。

而在其中非常重要的一个细节就是 字节顺序,发送方和接收方应该使用匹配的htonlhtons,每次想要通过socket进行传输信息都应该进行编码和解码。

接收方接受数据时,如何正确地遍历不同格式的缓冲区也非常重要,需要注意到read函数返回的是接收到的字节(8位)的个数!

1
2
3
4
5
int buf[];
int n = read(connfd, (char*)buf, 1024);
for(int i = 0; i * 4 < n; i++){
//do something on buf[]
}

为了使得服务器不仅能够接受不断地监听套接字提供服务,还能够通过键盘的键入来退出死循环、回收资源终止服务,上述程序还夹杂了非常多的私活。

  • 为了实现上述两个 不间断的服务,我们使用多线程。除去main函数中对套接字不断地监听,我们再开一个thread进行键盘的读入。

    起初不充分的设想是通过thread读取键盘,将输入的字符存储到全局变量op中,main函数在循环中不断地检查op是否为q.

    然而事实上listen会由于没有客户机的访问而一直堵塞,因而会卡在一次循环中无法得到op已经被修改的信息。

    那么考虑到这两个线程其实不存在必要的通信要求,只要thread读取到q就应该将套接字服务资源全部回收,终止进程,所以最终的实现就是在thread中回收资源并且终止进程,主函数不断地循环即可。

    不过之后由于main函数的循环监听中有可能有函数出错而终止服务,所以还是利用了op向thread传达信息

  • 听闻有函数getch可以读入键盘的输入,不需要按回车也不会在控制台回显,非常适合作为退出死循环的方法。然而好像getch不是标准库的函数,在linux下并没有实现,所以便自己实现了一个不回显、不需要空格 的键盘读入。基本原理是将控制台调整为原始模式,在原始模式下,所有的数据输入以字节为单位进行处理,当一个字节被输入后,便触发输入有效。那么将控制台设置为原始模式,完成getchar()后将终端模式恢复到原先的状态即可。具体实现参见header。

  • 上述的多线程函数带来了若干的问题。由上述分析可知,运行getch()后,当前终端会一直保持原始模式,那么如果此时main函数希望进行IO操作,输出的格式就会出现非常奇怪的问题(大概类似于阶梯状,不是很懂为什么)。所以我们还多写了一个自定义的printf函数,这个printf函数在输出前会先将终端设置为getch调用前的状态,输出完成后再将其到原始模式。因为一台机子上模拟CS不太会同时操作两个终端,所以这样基本能完成任务。但是这样实现仍然是非常朴素的,真正在设计网络服务器的时候一定要重新考虑

Unit 03

DNS

struct hostent

hostent是host entry的缩写,该结构记录主机的相关信息。

1
2
3
4
5
6
7
8
struct hostent{
char * h_name;
char ** h_aliases;
short h_addrtype;
short h_length;
char ** h_addr_list;
#define h_addr h_addr_list[0];
};

域名<->IP地址

1
2
struct hostent* gethostbyname(const char *name)
struct hostent *gethostbyaddr(const char *addr, size_t len, int family)

注意,通过IP获取主机信息时传入的参数addr不是IP字符串,而是经过inet_aton转化过的32位的网络序列地址。

1
int inet_aton(const char *string, struct in_addr*addr);

函数成功返回非零,失败返回0

recv与send

1
2
int recv(int sockfd,void* buf,int len, int flags);
int send(int sockfd,void* buf,int len, int flags);

len为发送/接受数据的长度

flags中可以填写一些参数

  • 0:相当于write和read
  • MSG_DONTROUTE:发送数据不查找路由表
  • MSG_OOB:发送、接受带外数据
  • MSG_PEEK:接受数据时不从缓冲区移走数据,其他进程仍然可以read到数据
  • MSG_WAITALL:数据量不够时,读操作等待

shutdown

1
int shutdown(int sockfd,int howto); 

参数howto指定关闭操作的类型:

  • howto = 0, 关闭读通道;丢弃尚未读取的数据,对后来接收到的数据返回确认后丢弃。
  • howto = 1, 关闭写通道;继续发送未发送完成的数据,然后发送FIN字段关闭写通道。
  • howto = 2, 关闭读写通道;任何进程都不能再操作这个socktet

shutdown与close的区别

close关闭当前进程与套接字的联系,其他进程仍然可能使用这个套接字。

shutdown直接在tcp层上操作该套接字,不管有多少个进程引用该套接字,当套接字关闭后试图读的进程会读到EOF,试图写的进程会检测到SIGPIPE信号。

readv与writev

readv为散布读,将文件中若干连续的数据块读入内存分散的缓冲区中。

writev为聚集写,即收集内存中分散的若干缓冲区中的数据写至文件的连续区域中。

1
2
3
4
5
6
7
struct iovec{
void * iov_base; /* 数据区的起始地址 */
size_t iov_len; /* 数据区的大小 */
};

ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);

参数iovcnt指出数组iov的元素个数,元素个数至多不超过IOV_MAX。linux中定义IOV_MAX的值为1024。

给出一个示例代码

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
//发送
char *name = " Wang Jin-pyng";
char *occupation = "Head of the legislative body";
int len[2];
struct iov[3];
int iovcnt;

len[0]=strlen(name);
len[1]=strlen(occupation);

iov[1].iov_base=name;关闭后
iov[1].iov_len=len[0];

iov[2].iov_base=occupation;
iov[2].iov_len=len[1];

len[0]=htonl(len[0]);
len[1]=htonl(len[1]);

iov[0].iov_base=len;
iov[0].iov_len=2*sizeof(int);

writev(sockfd,iov,3);
......

//接收
char name[1024];
char occupation[1024];
int len[2];
struct iov[2];
int iovlen;

......//
read(sockfd,len,2*sizeof(int));

len[0]=ntohl(len[0]);
len[1]=ntohl(len[1]);

iov[0].iov_base=name;
iov[0].iov_len=len[0];

iov[1].iov_base=occupation;
iov[1].iov_len=len[1];

readv(sockfd,iov,2);
......

//Note:通常还需要调用函数
//ioctl(fd,FIONREAD,nbyte),以确定接收缓存区内有多少个字节可读。

参考博客

readv()和writev()函数

recvmsg与sendmsg

这两个函数是最底层的函数,其余所有的IO均通过转化为该函数来进行。

具体参数不了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h>
#include <sys/socket.h>
int recvmsg(int sockfd,struct msghdr* msg,int flags);
int sendmsg(int sockfd,struct msghdr* msg,int flags);

struct msghdr{
void *msg_name;
int msg_namelen;
struct iovec* msg_iov;
int msg_iovlen;
void* msg_control;
int msg_controllen;
int msg_flags; //同recv、send
}

多路复用select

检查多个文件描述符(socket描述符)是否就绪,当某一个描述符就绪(可读、可写或发生异常)时函数返回。可以实现输入输出多路复用。

1
int select(int maxfd, fd_set *rdset, fd_set *wrest, fd_set *exset, struct timeval *timeout);

返回值:有描述符就绪则返回就绪的描述符个数;超时时间内没有描述符就绪返回0;执行失败返回-1。

参数:

  • maxfd-需要测试的描述符的最大值,实际测试的描述符从0-maxfd-1

  • rdset-需要测试是否可读的描述符集合(包括处于listen状态的socket接收到连接请求)

  • wrset-需要测试是否可写的描述符集合(包括以非阻塞方式调用connect是否成功)

  • exset-需要测试是否异常的描述符集合(包括接收带外数据的socket有带外数据到达)

  • timeout-指定测试超时的时间

    • timeval结构
    1
    2
    3
    4
    struct timeval{
    long tv_sec; //秒
    long tv_usec; //毫秒
    }
    • timeout=NULL,select将永远阻塞直到有一个描述符就绪,或者出现错误(接收到信号)。
    • timeout>0,在timeout时间内如果有描述符就绪则返回,否则在timeout时间后返回0;如果将3个描述符集合都设定为NULL则select相当于sleep函数,只是时间可以精确到毫秒
    • timeout=0,select检查完描述符集合后立即返回

操作描述符集合:

  • FD_ZERO(fd_set *fdset)-清空描述符集合
  • FD_SET(int fd, fd_set *fdset)-将一个描述符添加到描述符集合
  • FD_CLR(int fd, fd_set *fdset)-将一个描述符从描述符集合中清除
  • FD_ISSET(int fd, fd_set *fdset)-检测一个描述符是否就绪

在设置描述符集合前应该先调用FD_ZERO将集合清空,每次调用select函数前应该重新设置这3个集合

三个集合中的描述符可以交叉

不过现在的服务器一般不使用select,select本质好像是On地查询,效率不高,一般使用poll等函数方法

socket选项

获取或设置socket选项

1
2
int getsockopt(int sockfd, int level, int optname, void *optval,sock_len *optlen);
int setsockopt(int sockfd, int level, int optname, void *optval,sock_len optlen);

返回值: 0成功,-1失败

参数:

  • level 选项级别

    • SOL_SOCKET —通用socket选项
    • IPPROTO_IP—IP选项
    • IPPROTO_TCP—TCP选项
  • optname 选项名称

    • 一般通用socket选项

      • SO_KEEPALIVE

        设置该选项后,2小时内没有数据交换时,TCP协议将自动发送探测数据包,检查网络连接

      • SO_RCVBUF和SO_SNDBUF

        设置发送和接收数据缓冲区的大小(在连接建立以前设置)

      • SO_RCVTIMEO和SO_SNDTIMEO

        设置发送和接收超时,当指定时间内数据没有成功接收或发送,发送和接收函数将返回。

      • SO_REUSEADDR

        快速重启服务器程序

        启动服务器程序的多个实例(绑定本地IP地址的多个别名)

    • IP选项

      IP_HDRINCL:是否需要自己建立IP数据包首部,适用于原始socket

    • TCP选项

      • TCP_MAXSEG-TCP协议最大数据段长度
      • TCP_NODELAY-小数据包是否延迟发送(Nagle算法
  • optal选项值

  • opelen选项长度/存放选项长度的指针

fcntl

修改文件的属性。

1
int fcntl(int fd,int cmd,…)

返回值: 非负成功,-1失败

操作类型:

Cmd 参数 返回值 说明
F_GETFL 0 fd 获取描述符标志
F_SETFL O_NONBLOCK 0, -1 设置套接字为非阻塞式
F_GETOWN int* 0, -1 取得套接字的所有者
F_SETOWN int 0, -1 设置套接字的所有者

套接字的阻塞方式

设置socket为阻塞方式

1
2
3
4
int flags;
flags=fcntl(fd,F_GETFL,0);
flags|=O_NONBLOCK;
fcntl(fd,F_SETFL,flags);

设置socket为非阻塞方式

1
2
3
4
int flags;
flags=fcntl(fd,F_GETFL,0);
flags&=~O_NONBLOCK;
fcntl(fd,F_SETFL,flags);

一般情况下,socket默认是阻塞方式的,也就是说套接字会阻塞,直到客户机发送信息为止。

在非阻塞方式下,如果没有接收到消息套接字也会返回,那么主程序为了不断监听信息就得一直轮询套接字,占用大量的CPU资源。

而在多套接字情况下,由于阻塞方式的存在,多套接字可能不能很好的同时监听。

有一些解决方案

  • 多线程。但是线程需要占用资源,linux大概能开小几百的套接字线程。
  • IO多路复用:select、poll、epoll等,同时检测多个套接字是否就绪。

参考博客

为什么网络socket编程是阻塞的?因为非阻塞轮询占CPU,用多线程和IO复用

ioctl

1
int ioctl(int fd,int req,…);
req 参数 返回值 说明
SIOCATMARK int* 0, -1 是否到达带外标记
FIOASYNC int* 0, -1 异步I/O标志
FIONREAD int* 0, -1 缓存区中有多少字节数据可读