一、什么是socket?
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
(1)socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
(2)bind()函数
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
如ipv4对应的是:
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */};/* Internet address. */struct in_addr { uint32_t s_addr; /* address in network byte order */};
addrlen:地址长度
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
注意:网络字节序与主机字节序
(3)listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
listen 函数:sockfd为要监听的socket的描述字,backlog:socket连接的最大个数。
listen函数监听连接,connec函数为客户端用来与服务器创建连接。
(4)accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
socket中TCP三次握手:
客户端connect主动发送连接,向服务器发送SYN J包,然后阻塞等待服务器监听连接请求,并处理连接请求。服务器监听到连接请求并accept,这时进入accept阻塞状态,向客户端发送SYN K包,并确认请求ACK J+1;客户端收到服务器的确认信息后,然后connect返回确认,ACK K+1;服务器收到 ACK k+1,accept返回,三次握手连接成功。
socket中TCP四次挥手:
代码:
tcp_server.c
1 #include<stdio.h>
2 #include <stdlib.h>
3 #include<sys/types.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6 #include<netinet/in.h>
7 #include<errno.h>
8 #include<string.h>
9 #include<pthread.h>
10
11 const int back_log=5;
41 int start_up(char* ip,int port)
42 {
43 //sock
44 int sock=socket(AF_INET,SOCK_STREAM,0);
45 if(sock<0)
46 {
47 perror("sock");
48 exit(1);
49 }
50 struct sockaddr_in local;
51 local.sin_family=AF_INET;
52 local.sin_addr.s_addr=inet_addr(ip);
53 local.sin_port=htons(port);
54
55 //bind
56 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
57 {
58 perror("bind");
59 exit(2);
60 }
61
62 if(listen(sock,back_log)<0)
63 {
64 perror("listen");
65 exit(3);
66 }
67 return sock;
68 }
69
70 void usage(char* argv)
71 {
72 printf("%s:[ip][port]\n",argv);
73 }
74 int main(int argc,char* argv[])
75 {
76 if(argc!=3)
77 {
78 usage(argv[0]);
79 exit(1);
80 }
81 int port=atoi(argv[2]);
82 char* ip=argv[1];
83 int listen_sock=start_up(ip,port);
84 struct sockaddr_in client;
85 socklen_t len=sizeof(&client);
86 while(1)
87 {
88 int client_sock=accept(listen_sock,(struct sockaddr*)&client,&len);
89 if(client_sock<0)
90 {
91 perror("accept");
92 }
93 printf("[ip]:%s",inet_ntoa(client.sin_addr));
94 printf("[port]:%ld",ntohs(client.sin_port));
95 printf("get connection..\n");
96 #ifdef _v1_
97 char buf[1024];
98 while(1)
99 {
100 memset(buf,'\0',sizeof(buf));
101 ssize_t _s=read(client_sock,buf,sizeof(buf)-1);
102 if(_s>0)
103 {
104 buf[_s]='\0';
105 printf("client#\n");
106 printf("%s\n",buf);
107
108 }
109 else if(_s==0)
110 {
111
112 printf("client closed\n");
113 }
114 else
115 {
116 perror("read");
117 exit(4);
118 }
119
120 }
121 return 0;
122 }
tcp_client.c:
1 #include<sys/socket.h>
2 #include<sys/types.h>
3 #include<unistd.h>
4 #include<errno.h>
5 #include<string.h>
6 #include<arpa/inet.h>
7 #include<netinet/in.h>
8 #include<string.h>
9 #include<stdlib.h>
10 #include<stdio.h>
11
12
13 void usage(char* proc)
14 {
15 printf("Usage:%s[ip][port]\n",proc);
16 }
17 int main(int argc,char* argv[])
18 {
19 if(argc!=3)
20 {
21 usage(argv[0]);
22 exit(1);
23 }
24 char* ip=argv[1];
25 int port=atoi(argv[2]);
26
27 //socket
28 int sock=socket(AF_INET,SOCK_STREAM,0);
29 if(sock<0)
30 {
31 perror("sock");
32 exit(2);
33 }
34 struct sockaddr_in remote;
35 remote.sin_family=AF_INET;
36 remote.sin_port=htons(port);
37 remote.sin_addr.s_addr=inet_addr(ip);
38
39 int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote));
40 if(ret<0)
41 {
42 perror("coneect");
43 }
44
45 char buf[1024];
46 while(1)
47 {
48 memset(buf,'\0',sizeof(buf));
49 scanf("%s",buf);
50 ssize_t _s= write(sock,buf,sizeof(buf)-1);
51
52 if(_s<0)
53 {
54 perror("read");
55 }
56 }
57
58 return 0;
59 }
socket通信
服务器:
(1)创建socket描述字 socket();
(2)bind():绑定套接字到本地地址
(3)listen():监听是否有连接请求
(4)accept():接受连接
客户端:
(1)socket():创建套接字
(2)无需绑定,在发送请求时系统已经默认分配了一个端口号且将IP地址和端口号一并发送了过去;
server:
[admin@www socket]$ ./tcp_server 127.0.0.1 8080
[ip]:236.33.59.0[port]:38650get connection..
client#
we are young
client#
wo ai ni
client:
[admin@www socket]$ ./tcp_client 127.0.0.1 8080
we are young
wo ai ni
这种方法只能实现一个客户端连接,可以把监听连接和接收数据分隔开,分别交给两个进程去实现,子进程只负责接收数据,关闭监听连接文件描述符,父进程只负责监听。
注意:子进程退出,而父进程还在监听状态,就会造成僵尸进程,解决办法:如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。
代码:
122 pid_t id=fork();
123 if(id==0)
124 {
125 //child
126 close(listen_sock);
127 char buf[1024];
128 while(1)
129 {
130 memset(buf,'\0',sizeof(buf));
131 ssize_t _s=read(client_sock,buf,sizeof(buf)-1);
132 if(_s>0)
133 {
134 buf[_s-1]='\0';
135 printf("client#\n");
136 printf("%s\n",buf);
137
138 }
139 else if(_s==0)
140 {
141
142 printf("client closed\n");
143 }
144 else
145 {
146 perror("read");
147 }
148
149 close(client_sock);
150 exit(1);
151 }
152 }
153 else if(id>0)
154 {
155
156 close(client_sock);
157
158
159 }
160 else
161 {
162 perror("fork");
163 }
多线程实现:
165 pthread_t tid;
166 pthread_create(&tid,NULL,thread_run,(void*)client_sock);
167 pthread_detach(tid);
12 void* thread_run(void* argv)
13 {
14 int sock=(int)argv;
15 char buf[1024];
16 while(1)
17 {
18 memset(buf,'\0',sizeof(buf));
19 ssize_t _s=read(sock,buf,sizeof(buf)-1);
20 if(_s>0)
21 {
22 buf[_s]='\0';
23 printf("client#\n");
24 printf("%s\n",buf);
25
26 }
27 else if(_s==0)
28 {
29
30 printf("client closed\n");
31 }
32 else
33 {
34 perror("read");
35 exit(4);
36 }
37 }
38 close(sock);
39 return NULL;
40 }
线程分离,等到接收数据完毕后确认释放连接后,自动回收资源。
[ip]:169.138.4.8[port]:38656get connection..
client#
nihao
[ip]:127.0.0.1[port]:38657get connection..
client#
lall