socket编程
1 socket通信
想要了解socket就必须要知道计算机网络通信的TCP/IP协议族,首先TCP/IP协议族包括:运输层、网络层、链路层,而socket的位置如图所示,Socket是应用层与TCP/IP协议族通信的中间软件抽象层
Socket即为套接字,应用程序要为网络通信而创建一个套接字(socket)时,操作系统就返回一个值作为描述符来识别这个套接字,然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(从套接字中读取或写入数据)
网络socket通信的基本流程:
这个流程就相当于我们平时打电话给客服解决问题:
客服就是接电话的服务器端,首先客服开始上班( 创建socket() ),然后客服绑定一个电话( bind()流程 )作为工作电话,然后开始盯着这个电话听有没有人打进来( listen() 监听流程),没有人打进来就一直等着(accept()阻塞)
客户就是打电话的客户端,也同样拿起电话( 创建socket() ),打电话给客服( connect() 建立连接),这时候客户和客服就可以通话处理问题。
2 客户端程序
socket()
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而socket()用于创建一个socket描述符,它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写的操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符
int socket(int domain, int type, int protocol)
domain:即协议域,又称协议族(family),决定我们通信的类型
名称 | 通信格式 |
---|---|
AF_LOCAL(或称AF_UNIX) | 本地通信 |
AF_INET | IPV4 |
AF_INET6 | IPV6 |
type:指定socket类型,AF_INET对应数据流套接字(SOCK_STREAM),还有数据包套接字(SOCK_DGRAM),还有原始套接字(SOCK_RAW)等
protocol:就是指定协议,通过domain和type基本确定了新建的socket是什么类型的套接字,最后通过protocol来指定新建的socket到底支持哪一种协议,常用协议很多,当protocol为0时,会自动选择type类型对应的默认协议
返回值:成功返回当前最小的文件描述符(标准输出,标准输入和标准出错分别用了0,1和2,所以这里通常都会返回3.错误则返回 -1,并且可以调用strerror(errno)来打印错误内容。
connect()
TCP客户端程序调用socket()创建文件描述符sockfd之后,就可以调用connect()函数来连接服务器。如果客户端此时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户端的TCP连接,通过这两个文件描述符(客户端connect()的fd和服务器端accept()返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:客户端的socket()创建的描述字,即连接后数据往哪儿发
addr:要连接的服务器的socket地址信息,所以我们要定义一个struct sockaddr_in的结构体,填充服务器的IP地址及端口等信息
addrlen:socket地址的长度
返回值:成功返回0,失败返回-1,并且可以调用strerror(errno)打印错误内容
结构体填充
在调用connect()之前,我们需要先清空需要用到的结构体,然后设置服务器端的IP地址和端口信息到addr中
memset(&serv_addr, 0, sizeof(serv_addr)); //初始化结构体
serv_addr.sin_family = AF_INET; //设置通信类型,即IPV4
serv_addr.sin_port = htons(SERVE_PORT); //设置服务器端口,htons()函数是将整型变量从主机字节序变为网络字节序,就是整数在地址空间存储方式变为高位字节存储在内存的低地址处
inet_aton(AERVE_IP, &serv_addr.sin_addr); //此函数将点分十进制字符串转换为32位整型类型
客户端代码
1 #include <stdio.h>
2 #include <errno.h>
3 #include <string.h>
4 #include <sys/types.h>
5 #include <sys/socket.h>
6 #include <arpa/inet.h>
7 #include <unistd.h>
8 #include <stdlib.h>
9
10 #define MSG_STR "Hello,server!"
11
12 int main(int argc, char **argv)
13 {
14 int sockfd = -1;
15 int rv = -1;
16 int port;
17 char buf[1024];
18 char *servip;
19 struct sockaddr_in servaddr;
20
21 if(argc < 3)
22 {
23 printf("Program usage: %s [Server_IP] [Port]\n",argv[0]);
24 return -1;
25 }
26
27 servip = argv[1];
28 port = atoi(argv[2]);
29
30 sockfd = socket(AF_INET, SOCK_STREAM, 0);
31 if(sockfd < 0)
32 {
33 printf("Create socket failure: %s\n", strerror(errno));
34 return -1;
35 }
36 printf("Create socket[%d] successfully\n", sockfd);
37
38 memset(&servaddr, 0, sizeof(servaddr));
39 servaddr.sin_family = AF_INET;
40 servaddr.sin_port = htons(port);
41 inet_aton(servip, &servaddr.sin_addr);
42
43 rv = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
44 if(rv < 0)
45 {
46 printf("Connect to server[%s:%d] failed:%s\n", servip, port, strerror(errno));
47 return -2;
48 }
49 printf("Connect to server [%s:%d] successfully!\n", servip, port);
50
51 while(1)
52 {
53 rv = write(sockfd, MSG_STR, strlen(MSG_STR));
54 if(rv < 0)
55 {
56 printf("Write to server by sockfd[%d] failure: %s\n", sockfd, strerror(errno));
57 break;
58 }
59
60 memset(buf, 0, sizeof(buf));
61 rv = read(sockfd, buf, sizeof(buf));
62
63 if(rv < 0)
64 {
65 printf("Read data from server by sockfd[%d] failure: %s\n", sockfd, strerror(errno));
66 break;
67 }
68 else if(rv == 0)
69 {
70 printf("Socket[%d] get disconnected\n", sockfd);
71 break;
72 }
73 else if(rv > 0)
74 {
75 printf("Read %d bytes data from server: %s\n", rv, buf);
76 }
77
78 }
79
80 close(sockfd);
81 return 0;
82
83 }
3 服务器端程序
socket()
与客户端调用相同
bind()
当我们调用socket()创建一个socket时,返回的socket描述字它存在于协议族(address family, AF_XXX)空间中,但没有一个具体的地址。如果想要给他赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过他来连接服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用而是在connect()的由系统随机生成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能够使用特定的IP和端口来连接服务器了。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,bind()函数就是将这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最后都会强制类型转换后赋值给sockaddr这种类型的指针传给内核
addrlen:对应地址的长度
listen()
socket()函数创建的socket是一个主动类型的,如果一个作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,该函数将socket变为被动类型,等待客户端的连接请求
int listen(int sockfd, int backlog);
sockfd:socket()系统调用创建的要监听的socket描述字
backlog:相应socket可以在内核里排队的最大连接个数
accept()
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accept()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也就意味着如果没有客户端连接服务器的化该程序就会一直阻塞着不返回,直到由客户端连接为止。一旦有客户端调用了connect()函数就会触发服务器的accept()返回,这时整个TCP连接就建立好了
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:服务器开始调用socket()产生的描述字,称为监听socket描述字
*addr:用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等
addrlen:返回客户端协议地址的长度
accept函数返回的是由内核自动生成的一个全新的描述字(fd),代表了与返回客户的TCP连接,如果想发送数据给客户端,则我们可以调用write()等函数往该fd里面写内容即可,而如果想从客户端读取内容则调用read()等函数从该fd里读取数据即可。一个服务器通常仅仅创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了某个客户的服务,就应当把该客户对应的socket描述字关闭
服务器端代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <errno.h>
5 #include <unistd.h>
6 #include <sys/types.h>
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <arpa/inet.h>
10
11 #define MSG_STR "Hello,client!"
12 #define BACKLOG 13
13
14 int main(int argc, char **argv)
15 {
16 int sockfd = -1;
17 int rv = -1;
18 int client_fd = -1;
19 struct sockaddr_in serv_addr;
20 struct sockaddr_in cli_addr;
21 socklen_t cliaddr_len;
22 char buf[1024];
23 int port;
24
25 if(argc < 2)
26 {
27 printf("Program usage: %s [Server_port]\n", argv[0]);
28 return -1;
29 }
30 port = atoi(argv[1]);
33
34 sockfd = socket(AF_INET, SOCK_STREAM, 0);
35 if(sockfd < 0)
38 return -1;
39 }
40 printf("Create sockfd[%d] successfully\n", sockfd);
41
42 memset(&serv_addr, 0, sizeof(serv_addr));
43 serv_addr.sin_family = AF_INET;
44 serv_addr.sin_port = htons(port);
45 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
46
47 if(rv = bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
48 {
49 printf("Socket[%d] bind on port[%d] failure: %s\n", sockfd, port, strerror(errno));
50 return -2;
51 }
54 listen(sockfd, BACKLOG);
55 printf("Start to listen on port[%d]\n", port);
56
59 printf("Start waiting and accept new connect...\n");
60 client_fd = accept(sockfd, (struct sockaddr *)&cli_addr, &cliaddr_len);
61 if(client_fd < 0)
62 {
63 printf("Accept new client failed: %s\n",strerror(errno));
64 continue;
65 }
67
68 memset(buf, 0, sizeof(buf));
69
70 if((rv = read(client_fd, buf, sizeof(buf))) < 0)
71 {
72 printf("Read data from client socket[%d] failure: %s\n", client_fd, strerror(errno));
73 close(client_fd);
74 continue;
75 }
76 else if(rv == 0)
77 {
78 printf("client socket[%d] disconnected\n", client_fd);
79 close(client_fd);
80 continue;
81 }
82 printf("read %d bytes data from client [%d] and echo it back:' %s'\n", rv, client_fd, buf);
83
84 if((rv = write(client_fd, MSG_STR, strlen(MSG_STR))) < 0)
85 {
86 printf("Write %d bytes data back to client[%d] failure: %s\n", rv, client_fd, strerror(errno))
;
87 close(client_fd);
88 continue;
89 }
90
91 printf("Close client socket[%d]\n",client_fd);
92 sleep(1);
93 close(client_fd);
94 }
95
96 close(sockfd);
97 return 0;
98
99 }
3 运行结果
首先启动服务器端 (等待连接)
再启动客户端请求连接 (连接成功,并读取到服务器端的数据)
客户端成功连接服务器端 (连接成功,并读取到客户端的数据)
连接完成后继续阻塞,等待下一个客户端的请求连接。。。