Linux 下的 socket 编程
基础知识
socket常用函数
socket
domain指定协议族,type指定socket类型,protocol指定协议(为0时自动选择type类型对应的默认协议)
1int socket(int domain, int type, int protocol);
返回值大于0成功;-1失败,错误类型保存在全局变量errno中。
bind
分配地址族中的特地地址给socket sockfd为socket描述字,通过socket函数的返回值确定 addr指向绑定给sockfd的地址,根据socket创建时协议族的不同而不同。
1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值等于0成功;-1失败,错误类型保存在全局变量errno中。
sockaddr*可能的若干结构
ipv4
1struct sockaddr_in {
2 sa_family_t sin_family;
3 in_port_t sin_port;
4 struct in_addr sin_addr;
5};
6
7struct in_addr {
8 uint32_t s_addr;
9};
ipv6
1struct sockaddr_in6 {
2 sa_family_t sin6_fami一个ly;
3 in_port_thtonl()--"Host to Network Long"
4ntohl()--"Network to Host Long"
5htons()--"Host to Network Short"
6ntohs()--"Network to Host Short" sin6_port;
7 uint32_t sin6_flowinfo;
8 struct in6_addr sin6_addr;
9 uint32_t sin6_scope_id;
10};
11
12struct in6_addr {
13 unsigned char s6_addr[16];
14};
listen
服务器监听指定的套接字,backlog指定socket最大连接个数。
1int listen(int sockfd, int backlog);
返回值等于0成功;-1失败,错误类型保存在全局变量errno中。
connect
客户端调用connect函数与服务器进行连接。
sockfd为客户机的套接字描述符,addr为服务器的套接字地址,addrlen为socket地址的长度。
1int 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为协议地址的长度,返回连接套接字的描述字。
1int 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)
1ssize_t read(int fd, void *buf, size_t count);
2ssize_t write(int fd, const void *buf, size_t count);
close
将套接字引用计数-1
close后进程不再能访问该套接字,但套接字的清除由TCP延后完成。
1int close(int sockfd);
getsockname, getpeername
获取套接字的名字。
1int getsockname(int fd,struct sockaddr* localaddr,int* addrlen);
2int 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”
参考博客
Unit 02
简单的交互示例
client向server发送三条消息hello hello1 hello2
server收到消息后回复一个welcome
server.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <arpa/inet.h>
7#include <unistd.h>
8#include <sys/types.h>
9
10#define MAXDATASIZE 128
11#define PORT 3000
12#define BACKLOG 5
13
14int main(int argc,char **argv){
15 int sockfd, new_fd, nbytes, sin_size;
16 char buf[MAXDATASIZE];
17 struct sockaddr_in srvaddr, clientaddr;
18
19 //1.创建网络端点
20 sockfd=socket(AF_INET,SOCK_STREAM,0);
21 if(sockfd==-1){
22 printf("can;t create socket\n");
23 exit(1);
24 }
25
26 //套接字选项 增添端口地址可重用选项,使得某台服务器崩溃后可以快速用其余服务器重新提供服务
27 if(argc==2){
28 int on=1;
29 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
30 printf("reuse addr\n");
31 }
32
33 //填充地址
34 bzero(&srvaddr,sizeof(srvaddr));
35 srvaddr.sin_family=AF_INET;
36 //htons进行字节顺序转换
37 srvaddr.sin_port=htons(PORT);
38 //服务器上无论有多少个IP发来消息,全部都予以接受
39 srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
40
41 //将xxx.xxx.xxx.xxx转换为32位的地址
42 /*
43 if(inet_aton(argv[1],&srvaddr.sin_addr)==-1){
44 printf("addr convert error\n");
45 exit(1);
46 }
47 */
48 //2.绑定服务器地址和端口
49 if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
50 printf("bind error\n");
51 exit(1);
52 }
53 //3. 监听端口,将主动套接字转换为被动套接字
54 if(listen(sockfd,BACKLOG)==-1){
55 printf("listen error\n");
56 exit(1);
57 }
58 //一般应该在循环中添加一些退出的方法
59 for(;;){
60 //4.接受客户端连接
61 sin_size=sizeof(struct sockaddr_in);
62 if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr, (socklen_t*)&sin_size))==-1){
63 printf("accept error\n");
64 continue;
65 }
66 //如果成功连接,那么打印出客户机的ip、port
67 printf("client addr:%s %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
68
69 //5.接收请求
70 //延时 必须读入一个字符才继续 可以检查读入的客户机数据的情况(但是很麻烦所以我删了)
71 //getchar();
72
73 //sockfd属于监听套接字,new_fd属于连接套接字
74 nbytes=read(new_fd,buf,MAXDATASIZE);
75 buf[nbytes]='\0';
76 printf("client:%s\n\n",buf);
77
78 //6.回送响应
79 sprintf(buf,"wellcome!");
80 write(new_fd,buf,strlen(buf));
81
82 //关闭socket
83 close(new_fd);
84 }
85 close(sockfd);
86
87 return 0;
88}
client.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <errno.h>
7#include <netdb.h>
8#include <arpa/inet.h>
9#include <unistd.h>
10
11#define MAXDATASIZE 128
12#define PORT 3000
13
14int addr_conv(char *address,struct in_addr *inaddr);
15
16int main(int argc,char **argv){
17 int sockfd,nbytes;
18 int port=PORT;
19 char buf[MAXDATASIZE];
20 struct sockaddr_in srvaddr;
21
22 //必须输入1/2个参数,具体格式参见printf的内容
23 if(argc!=2 && argc!=3){
24 printf("usage:./client hostname|ip. Or usage:./client hostname|ip port\n");
25 exit(0);
26 }
27 //如果有指定端口号则使用,不然使用默认端口PORT=3000
28 if(argc==3) port=atoi(argv[2]);
29
30 //1.创建网络端点
31 sockfd=socket(AF_INET,SOCK_STREAM,0);
32 if(sockfd==-1){
33 printf("can;t create socket\n");
34 exit(1);
35 }
36 //指定服务器协议簇,端口(本地socket地址采用默认值)
37 bzero(&srvaddr,sizeof(srvaddr));
38 srvaddr.sin_family=AF_INET;
39 srvaddr.sin_port=htons(port);
40 /*
41 if(inet_aton("127.0.0.1",&srvaddr.sin_addr)==-1){
42 printf("addr convert error\n");
43 exit(1);
44 }
45 */
46 //根据传入参数指定服务器IP地址
47 if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
48 perror(strerror(errno));
49 }
50 //2.连接服务器
51 //客户端的IP地址等信息不需要手动填写,由内核自动完成
52 if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
53 printf("connect error\n");
54 exit(1);
55 }
56
57 //3.发送请求
58 sprintf(buf,"hello"); write(sockfd,buf,strlen(buf));
59 sprintf(buf,"hello2"); write(sockfd,buf,strlen(buf));
60 sprintf(buf,"hello3"); write(sockfd,buf,strlen(buf));
61
62 //4.接收响应
63 if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
64 printf("read error\n");
65 exit(1);
66 }
67 buf[nbytes]='\0';
68 printf("srv respons:%s\n\n",buf);
69
70 //关闭socket
71 close(sockfd);
72 return 0;
73}
74
75int addr_conv(char *address,struct in_addr *inaddr){
76 struct hostent *he;
77 //inet_aton 函数将一个“字符串类型”的IP地址转化为“整数类型”的地址
78 if(inet_aton(address,inaddr)==1){
79 printf("call inet_aton sucess.\n");
80 return 0;
81 }
82 printf("call inet_aton fail.\n");
83 //如果输入的不是IP地址而主机名/域名,那么可以通过这些来获得IP地址
84 he=gethostbyname(address);
85 if(he!=NULL){
86 printf("call gethostbyname sucess.\n");
87 *inaddr=*((struct in_addr *)(he->h_addr_list[0]));
88 return 0;
89 }
90 return -1;
91}
套接字最大创建数量
client_msock.c(主动套接字最大数量)
检查最多可创建的套接字数量 向程序传入两个参数,第一个参数为IP地址,第二个参数为想要创建的套接字数量 具体实现其实是On地尝试创建所有希望的套接字,出错则退出。
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <errno.h>
7#include <netdb.h>
8#include <arpa/inet.h>
9#include <arpa/inet.h>
10#include <unistd.h>
11
12#define MAXDATASIZE 128
13#define PORT 3000
14
15int addr_conv(char *address,struct in_addr *inaddr);
16
17
18int main(int argc,char **argv){
19 int i,sockfd;
20 struct sockaddr_in srvaddr;
21 int n=atoi(argv[2]);
22
23 //1.创建网络端点
24 for(i=0;i<n;i++)
25 {
26 sockfd=socket(AF_INET,SOCK_STREAM,0);
27 if(sockfd==-1){
28 printf("Failed to create the %d socket.\n",i);
29 exit(1);
30 }
31 //指定服务器地址(本地socket地址采用默认值)
32 bzero(&srvaddr,sizeof(srvaddr));
33 srvaddr.sin_family=AF_INET;
34 srvaddr.sin_port=htons(PORT);
35
36 if(addr_conv(argv[1],&srvaddr.sin_addr)==-1){
37 perror(strerror(errno));
38 }
39 }
40
41 printf("Finished.\n");
42 return 0;
43}
server_msock.c(被动套接字最大数量)
传入2/4个参数,表示起始端口号、创建连续套接字数量、发送信息大小、接受信息大小。
具体实现与上述类似,暴力模拟,检验是否可行。
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <arpa/inet.h>
7#include <unistd.h>
8#include <sys/types.h>
9
10#define MAXDATASIZE 128
11#define BACKLOG 5
12
13int main(int argc,char **argv){
14 int sockfd,new_fd,nbytes,sin_size;
15 char buf[MAXDATASIZE];
16 struct sockaddr_in srvaddr;
17 int i,lastsockfd=-1;
18 int minport,n,sizeSND,sizeRCV;
19
20 if(argc!=3 && argc!=5)
21 {
22 printf("You should input 3 or 5 params: ");
23 printf("func, startPort, number, SND size, RCV size.\n");
24 exit(1);
25 }
26
27 minport=atoi(argv[1]);
28 n=atoi(argv[2]);
29 if(argc==5)
30 {
31 sizeSND=atoi(argv[3]);
32 sizeRCV=atoi(argv[4]);
33 }
34
35 for(i=0;i<n;i++)
36 {
37 //1.创建网络端点
38 sockfd=socket(AF_INET,SOCK_STREAM,0);
39 if(sockfd==-1){
40 printf("Failed to create %d socket.The last sockfd is %d.\n",i,lastsockfd);
41 exit(1);
42 }
43 lastsockfd=sockfd;
44
45 if(argc==5){ // get & set SO_SNDBUF,SO_RCVBUF
46 int size;
47 socklen_t size2=sizeof(size);
48 if((getsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&size,&size2))<0)
49 {
50 printf("getsockopt failed\n");
51 exit(1);
52 }
53 //printf("SND buff is %d\n",size);
54 if((getsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,&size2))<0)
55 {
56 printf("getsockopt failed\n");
57 exit(1);
58 }
59 //printf("RCV buff is %d\n",size);
60
61 if((setsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&sizeSND,sizeof(sizeSND)))<0
62 || (setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&sizeRCV,sizeof(sizeRCV)))<0)
63 {
64 printf("setsockopt failed.\n");
65 exit(1);
66 }
67// printf("New SND and RCV buff are %d and %d.\n",sizeSND,sizeRCV);
68// exit(0);
69 }
70
71 //填充地址
72 bzero(&srvaddr,sizeof(srvaddr));
73 srvaddr.sin_family=AF_INET;
74 srvaddr.sin_port=htons(minport+i);
75 srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
76
77 //2.绑定服务器地址和端口
78 if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
79 printf("bind error\n");
80 exit(1);
81 }
82 //3. 监听端口
83 if(listen(sockfd,BACKLOG)==-1){
84 printf("listen error\n");
85 exit(1);
86 }
87 }
88
89 printf("Finished.The last sockfd is %d.\n",sockfd);
90 return 0;
91}
统计自由端口数量
具体实现是客户端尝试进行充分多次的套接字连接请求,服务器端每次根据发送方的IP地址更新最大最小值,这样就能知道自由端口所在的区间。.
大概自由端口保证是连续的
client_port.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <errno.h>
7#include <netdb.h>
8#include <arpa/inet.h>
9#include <unistd.h>
10
11#define MAXDATASIZE 128
12#define PORT 3000
13
14int addr_conv(char *address,struct in_addr *inaddr);
15
16int main(int argc,char **argv){
17 int sockfd,nbytes,i,j;
18 char buf[MAXDATASIZE];
19 struct sockaddr_in srvaddr;
20 if(argc!=2){
21 printf("usage:./client hostname|ip\n");
22 exit(0);
23 }
24
25 bzero(&srvaddr,sizeof(srvaddr));
26 srvaddr.sin_family=AF_INET;
27 srvaddr.sin_port=htons(PORT);
28 if(addr_conv(argv[1],&srvaddr.sin_addr)==-1)
29 {
30 perror(strerror(errno));
31 exit(1);
32 }
33
34 for(j=0;j<100;j++)
35 {
36 for(i=0;i<10000;i++)
37 {
38 sockfd=socket(AF_INET,SOCK_STREAM,0);
39 if(sockfd==-1){
40 printf("can;t create socket\n");
41 exit(1);
42 }
43 if(connect(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
44 printf("connect error\n");
45 exit(1);
46 }
47
48 sprintf(buf,"hello");
49 write(sockfd,buf,strlen(buf));
50 if((nbytes=read(sockfd,buf,MAXDATASIZE))==-1){
51 printf("read error\n");
52 exit(1);
53 }
54 close(sockfd);
55 }
56
57 printf("srv respons 10k times:%d\n",j);
58 }
59
60 return 0;
61}
server_port.c
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/socket.h>
5#include <netinet/in.h>
6#include <arpa/inet.h>
7#include <unistd.h>
8#include <sys/types.h>
9
10#define MAXDATASIZE 128
11#define PORT 3000
12#define BACKLOG 5
13
14int main(int argc,char **argv){
15 int sockfd, new_fd, nbytes, sin_size;
16 unsigned int port, minport, maxport;
17 char buf[MAXDATASIZE];
18 struct sockaddr_in srvaddr,clientaddr;
19
20 //1.创建网络端点
21 sockfd=socket(AF_INET,SOCK_STREAM,0);
22 if(sockfd==-1){
23 printf("can;t create socket\n");
24 exit(1);
25 }
26
27 if(argc==2){
28 int on=1;
29 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
30 printf("reuse addr\n");
31 }
32 //填充地址
33 bzero(&srvaddr,sizeof(srvaddr));
34 srvaddr.sin_family=AF_INET;
35 srvaddr.sin_port=htons(PORT);
36 srvaddr.sin_addr.s_addr=htonl(INADDR_ANY);
37
38 //2.绑定服务器地址和端口
39 if(bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr))==-1){
40 printf("bind error\n");
41 exit(1);
42 }
43 //3. 监听端口
44 if(listen(sockfd,BACKLOG)==-1){
45 printf("listen error\n");
46 exit(1);
47 }在执行 make 之
48
49 minport=65535;
50 maxport=0;
51 printf("Listening......\n");
52 for(;;){
53 //4.接受客户端连接
54 sin_size=sizeof(struct sockaddr_in);
55 if((new_fd=accept(sockfd,(struct sockaddr *)&clientaddr,&sin_size))==-1){
56 printf("accept error\n");
57 continue;
58 }
59
60 port=(unsigned int)ntohs(clientaddr.sin_port);
61 if(port<minport || port>maxport)
62 {
63 if(port<minport)
64 minport=port;
65 else if(port>maxport)
66 maxport=port;
67
68 printf("minport is: %d\t",minport);
69 printf("maxport is: %d\n",maxport);
70 }
71 nbytes=read(new_fd,buf,MAXDATASIZE);
72 write(new_fd,buf,strlen(buf));
73 close(new_fd);
74 }
75 close(sockfd);
76
77 return 0;
78}
:star: 客户端发送整数,服务器计算平均值后返回
header
1#include <termio.h>
2#include <pthread.h>
3#include <stdarg.h>
4
5#define itn int
6#define whlie while
7#define fro for
8#define pritnf printf
9
10struct termios tmtemp, tm;
11char getch(void){
12 int c = 0, fd = 0;
13
14 //获取当前的终端属性设置,并保存到tm结构体中
15 if(tcgetattr(fd,&tm) != 0) return -1;
16
17 tmtemp=tm;
18
19 //将tetemp初始化为终端原始模式的属性设置
20 cfmakeraw(&tmtemp);
21
22 //终端设置为原始模式的设置
23 if(tcsetattr(fd,TCSANOW,&tmtemp) != 0) return -1;
24
25 c=getchar();
26
27 if(tcsetattr(fd,TCSANOW,&tm) != 0) return -1;
28
29 return (char)c;
30}
31
32int myprintf(char *format, ...)
33{
34 struct termios now;
35 if(tcgetattr(0, &now) != 0) return -1;
36 if(tcsetattr(0,TCSANOW,&tm) != 0) return -1;
37
38 va_list args;
39 va_start(args, format);
40 int cnt = vprintf(format, args);
41 va_end(args);
42
43 if(tcsetattr(0,TCSANOW,&now) != 0) return -1;
44 return cnt;
45}
client
1#include <string.h>
2#include <sys/socket.h>
3#include <netinet/in.h>
4#include <stdio.h>
5#include <signal.h>
6#include <stdlib.h>
7#include <errno.h>
8#include <netdb.h>
9#include <arpa/inet.h>
10#include <unistd.h>
11#include <time.h>
12
13#define SERVER_PORT 8086
14
15int n, buf[1024], msg[1024];
16
17int fun(char *ip){
18 int sockfd;
19 struct sockaddr_in servaddr;
20 sockfd = socket(AF_INET, SOCK_STREAM, 0);
21 if(sockfd < 0){
22 fprintf(stderr, "Socket error\n");
23 return -1;
24 }
25
26 bzero(&servaddr, sizeof servaddr);
27 servaddr.sin_family = AF_INET;
28 servaddr.sin_port = htons(SERVER_PORT);
29 if(inet_aton(ip, &servaddr.sin_addr) == -1){
30 fprintf(stderr,"Inet_aton error\n");
31 return -1;
32 }
33
34 if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0){
35 fprintf(stderr,"Connect error\n");
36 close(sockfd);
37 return -1;
38 }
39
40 msg[0] = htonl(n);
41 for(int i = 0; i < n; i++) msg[i + 1] = htonl(buf[i]); msg[n + 1] = 0;
42
43 write(sockfd, msg, (n + 1) * sizeof(int));
44
45 n = (int)read(sockfd, msg, 1024);
46 if(n < 0){
47 fprintf(stderr, "Read error\n");
48 return -1;
49 }
50 printf("服务器返回平均值为%d\n", ntohl(msg[0]));
51 close(sockfd);
52 return 0;
53}
54
55int main(int argc, char *argv[]){
56 if(argc != 2){
57 puts("请输入一个参数表示服务器IP地址!");
58 return 0;
59 }
60
61 srand((int)time(NULL));
62
63 n = 255;
64 for(int i = 0; i < n; i++) buf[i] = rand() % 100;
65 printf("总共产生%d个数字:\n", n);
66 for(int i = 0; i < n; i++) printf("%d ", buf[i]); puts("");
67
68 fun(argv[1]);
69
70 return 0;
71}
server
1#include <string.h>
2#include <sys/socket.h>
3#include <netinet/in.h>
4#include <stdio.h>
5#include <signal.h>
6#include <stdlib.h>
7#include <errno.h>
8#include <netdb.h>
9#include <arpa/inet.h>
10#include <unistd.h>
11#include <time.h>
12
13#include "rqdmap.h"
14
15
16#define SERVER_PORT 8086
17#define BACKLOG 5
18
19char op;
20void * thread(void * listenfd){
21 while(op != 'q'){
22 fflush(stdin);
23 op = getch();
24 printf("receive %d\n", op);
25 }
26 close(*(int *)listenfd);
27 exit(1);
28}
29
30int buf[1030];
31
32
33int fun(int connfd){
34 int n = (int)read(connfd, (char*)buf, 1100);
35 if(n < 0){
36 fprintf(stderr,"Read error");
37 return -1;
38 }
39 n = ntohl(buf[0]);
40 int sum = 0;
41
42 // for(int i = 0; i < n; i++) printf("%d ", ntohl(buf[i + 1]));
43 for(int i = 1; i <= n; i++) sum += ntohl(buf[i]);
44 sum /= n;
45
46 if(myprintf("服务器输出 %d \n", sum) < 0){
47 fprintf(stderr, "Print error\n");
48 return -1;
49 }
50
51 buf[0] = htonl(sum); buf[1] = 0;
52 write(connfd, buf, sizeof(int));
53 return 0;
54}
55
56
57int main(){
58 int listenfd, connfd;
59 struct sockaddr_in servaddr;
60
61 listenfd = socket(AF_INET, SOCK_STREAM, 0);
62 printf("产生套接字 %d\n", listenfd);
63 if(listenfd < 0){
64 fprintf(stderr, "Socket error\n");
65 exit(1);
66 }
67
68 int on = 1;
69 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
70
71 bzero(&servaddr, sizeof servaddr);
72 servaddr.sin_family = AF_INET;
73 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
74 servaddr.sin_port=htons(SERVER_PORT);
75 if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof (struct sockaddr)) < 0){
76 fprintf(stderr,"Bind error\n");
77 exit(1);
78 }
79
80 if(listen(listenfd, 5) < 0){
81 fprintf(stderr,"Listen error\n");
82 close(listenfd);
83 exit(1);
84 }
85
86
87 pthread_t pid;
88 if(pthread_create(&pid, NULL, thread, &listenfd) != 0){
89 puts("Create pthread error!");
90 exit(1);
91 }
92
93 while(1){
94 connfd = accept(listenfd, NULL, NULL);
95 if(connfd < 0){
96 fprintf(stderr,"Accept error\n");
97 exit(1);
98 }
99
100 if(fun(connfd) != 0){
101 fprintf(stderr, "fun error\n");
102 op = 'q'; break;
103 }
104 close(connfd);
105 }
106
107 pthread_join(pid, NULL);
108 return 0;
109}
实战记录
遇到了非常多的坑点…弄了很久
利用socket套接字进行数据通信最关键的地方其实在于 通信格式的设计,这样接收方才能够正确无误地解读发送方的比特串。
而在其中非常重要的一个细节就是 字节顺序,发送方和接收方应该使用匹配的htonl
或htons
,每次想要通过socket进行传输信息都应该进行编码和解码。
接收方接受数据时,如何正确地遍历不同格式的缓冲区也非常重要,需要注意到read函数返回的是接收到的字节(8位)的个数!
1int buf[];
2int n = read(connfd, (char*)buf, 1024);
3for(int i = 0; i * 4 < n; i++){
4 //do something on buf[]
5}
为了使得服务器不仅能够接受不断地监听套接字提供服务,还能够通过键盘的键入来退出死循环、回收资源终止服务,上述程序还夹杂了非常多的私活。
-
为了实现上述两个 不间断的服务,我们使用多线程。除去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的缩写,该结构记录主机的相关信息。
1struct hostent{
2 char * h_name;
3 char ** h_aliases;
4 short h_addrtype;
5 short h_length;
6 char ** h_addr_list;
7 #define h_addr h_addr_list[0];
8};
域名<->IP地址
1struct hostent* gethostbyname(const char *name)
2struct hostent *gethostbyaddr(const char *addr, size_t len, int family)
注意,通过IP获取主机信息时传入的参数addr不是IP字符串,而是经过inet_aton转化过的32位的网络序列地址。
1int inet_aton(const char *string, struct in_addr*addr);
函数成功返回非零,失败返回0
recv与send
1int recv(int sockfd,void* buf,int len, int flags);
2int send(int sockfd,void* buf,int len, int flags);
len为发送/接受数据的长度
flags中可以填写一些参数
- 0:相当于write和read
- MSG_DONTROUTE:发送数据不查找路由表
- MSG_OOB:发送、接受带外数据
- MSG_PEEK:接受数据时不从缓冲区移走数据,其他进程仍然可以read到数据
- MSG_WAITALL:数据量不够时,读操作等待
shutdown
1int 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为聚集写,即收集内存中分散的若干缓冲区中的数据写至文件的连续区域中。
1struct iovec{
2 void * iov_base; /* 数据区的起始地址 */
3 size_t iov_len; /* 数据区的大小 */
4};
5
6ssize_t readv(int fildes, const struct iovec *iov, int iovcnt);
7ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);
参数iovcnt指出数组iov的元素个数,元素个数至多不超过IOV_MAX。linux中定义IOV_MAX的值为1024。
给出一个示例代码
1//发送
2char *name = " Wang Jin-pyng";
3char *occupation = "Head of the legislative body";
4int len[2];
5struct iov[3];
6int iovcnt;
7
8len[0]=strlen(name);
9len[1]=strlen(occupation);
10
11iov[1].iov_base=name;关闭后
12iov[1].iov_len=len[0];
13
14iov[2].iov_base=occupation;
15iov[2].iov_len=len[1];
16
17len[0]=htonl(len[0]);
18len[1]=htonl(len[1]);
19
20iov[0].iov_base=len;
21iov[0].iov_len=2*sizeof(int);
22
23writev(sockfd,iov,3);
24......
25
26//接收
27char name[1024];
28char occupation[1024];
29int len[2];
30struct iov[2];
31int iovlen;
32
33......//
34read(sockfd,len,2*sizeof(int));
35
36len[0]=ntohl(len[0]);
37len[1]=ntohl(len[1]);
38
39iov[0].iov_base=name;
40iov[0].iov_len=len[0];
41
42iov[1].iov_base=occupation;
43iov[1].iov_len=len[1];
44
45readv(sockfd,iov,2);
46......
47
48//Note:通常还需要调用函数
49//ioctl(fd,FIONREAD,nbyte),以确定接收缓存区内有多少个字节可读。
参考博客
recvmsg与sendmsg
这两个函数是最底层的函数,其余所有的IO均通过转化为该函数来进行。
具体参数不了解。
1#include <sys/types.h>
2#include <sys/socket.h>
3int recvmsg(int sockfd,struct msghdr* msg,int flags);
4int sendmsg(int sockfd,struct msghdr* msg,int flags);
5
6struct msghdr{
7 void *msg_name;
8 int msg_namelen;
9 struct iovec* msg_iov;
10 int msg_iovlen;
11 void* msg_control;
12 int msg_controllen;
13 int msg_flags; //同recv、send
14}
多路复用select
检查多个文件描述符(socket描述符)是否就绪,当某一个描述符就绪(可读、可写或发生异常)时函数返回。可以实现输入输出多路复用。
1int 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结构
1struct timeval{ 2 long tv_sec; //秒 3 long tv_usec; //毫秒 4}
- 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选项
1int getsockopt(int sockfd, int level, int optname, void *optval,sock_len *optlen);
2int 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
修改文件的属性。
1int 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为阻塞方式
1int flags;
2flags=fcntl(fd,F_GETFL,0);
3flags|=O_NONBLOCK;
4fcntl(fd,F_SETFL,flags);
设置socket为非阻塞方式
1int flags;
2flags=fcntl(fd,F_GETFL,0);
3flags&=~O_NONBLOCK;
4fcntl(fd,F_SETFL,flags);
一般情况下,socket默认是阻塞方式的,也就是说套接字会阻塞,直到客户机发送信息为止。
在非阻塞方式下,如果没有接收到消息套接字也会返回,那么主程序为了不断监听信息就得一直轮询套接字,占用大量的CPU资源。
而在多套接字情况下,由于阻塞方式的存在,多套接字可能不能很好的同时监听。
有一些解决方案
- 多线程。但是线程需要占用资源,linux大概能开小几百的套接字线程。
- IO多路复用:select、poll、epoll等,同时检测多个套接字是否就绪。
参考博客
为什么网络socket编程是阻塞的?因为非阻塞轮询占CPU,用多线程和IO复用
ioctl
1int ioctl(int fd,int req,…);
req | 参数 | 返回值 | 说明 |
---|---|---|---|
SIOCATMARK | int* | 0, -1 | 是否到达带外标记 |
FIOASYNC | int* | 0, -1 | 异步I/O标志 |
FIONREAD | int* | 0, -1 | 缓存区中有多少字节数据可读 |