Skip to content

Commit d1a30d5

Browse files
committed
doc 3
1 parent 7d644c7 commit d1a30d5

File tree

1 file changed

+300
-9
lines changed

1 file changed

+300
-9
lines changed

c/study/doc_3_vim_socket.md

Lines changed: 300 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,308 @@ pathogen.vim, 如下
104104

105105
### TCP基础
106106

107-
建立连接,关闭连接,滑动窗口
107+
TCP使用很广泛,先了解一下概念,TCP是面向连接的协议,所以有建立连接和关闭连接
108+
的过程。
108109

109-
### linux基础
110+
建立连接过程需要三步握手,如下:
111+
112+
1. A向B发送syn信令
113+
2. B向A回复ack,以及发送sync信令
114+
3. A向B回复ack
115+
116+
其实网络上发送数据都有可能丢的,所以每个发送给对端的数据,要收到答复才能确认
117+
对方收到了。
118+
比如上面第二步A收到了B返回的ack才能确认连接已经建立成功,自己给B发送数据,B
119+
可以收到,同样第三步B收到A的ack才能确认连接建立成功,自己发个A的数据,A能收到。
120+
所以TCP连接建立不是两步握手,不是四步握手,而是三步握手。
110121

111-
库函数,libc里,
112-
头文件位置
113-
size_t, ssize_t
114-
文件描述符, read, write
115-
系统调用
116-
errno
122+
连接建立成功后双方就可以互发psh信令来传输数据了,同样发出去的psh数据,也需要
123+
收到ack才能确认对方收到,否则就得等待超时后重发。
124+
125+
拆除连接需要四步握手, 因为TCP是双工的,所以自己这边关闭连接,有可能对方还会
126+
给自己发数据,还得等对方说自己不会给自己发送数据了。
127+
128+
1. A向B发送fin, 表示自己没有数据向B发送了。
129+
2. B向A回复ack
130+
3. B向A发送过fin, 表示自己没有数据向A发送了。
131+
4. A向B回复ack
132+
133+
另外就是在任何时候都可能收到对方发来的rst信令,表示直接复位该连接,也别发数据了
134+
也别等着收数据了,赶紧把资源都回收了吧。
135+
136+
TCP还有滑动窗口的流量控制机制,以及各种超时处理逻辑,有兴趣的话具体细节看
137+
《TCP/IP协议详解》了。
138+
139+
linux下用tcpdump可以抓包学习TCP协议,比如在执行`curl -I www.baidu.com`时用
140+
tcpdump抓包如下。
141+
142+
# tcpdump -nn -t host www.baidu.com
143+
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
144+
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
145+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [S], seq 1772495094, win 14600, options [mss 1460,sackOK,TS val 214360452 ecr 0,nop,wscale 5], length 0
146+
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [S.], seq 946873815, ack 1772495095, win 14600, options [mss 1440,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 7], length 0
147+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 1, win 457, length 0
148+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [P.], seq 1:168, ack 1, win 457, length 167
149+
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 168, win 202, length 0
150+
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [P.], seq 1:705, ack 168, win 202, length 704
151+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 705, win 501, length 0
152+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [F.], seq 168, ack 705, win 501, length 0
153+
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 169, win 202, length 0
154+
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [F.], seq 705, ack 169, win 202, length 0
155+
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 706, win 501, length 0
156+
157+
可以看到本机的ip是10.190.176.177,baidu解析出来的ip是180.97.33.71,然后前三个
158+
包就是建立连接的三步握手,最后三个包是关闭连接的四步握手。中括号里的S表示sync,
159+
p表示psh,F表示fin,.好像表示ack。
160+
161+
162+
### Linux基础
163+
164+
其实Linux下,C的库函数,以及linux API都在libc.so里面,没有分开
165+
的。玩C语言开发,肯定要对C库函数和常用的linux API有所熟悉的,可以先看
166+
如下两个链接快速了解一下,知道系统有哪些能力和轮子。
167+
168+
Standard C 语言标准函数库速查
169+
http://ganquan.info/standard-c/
170+
Linux系统调用列表
171+
http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html
172+
173+
再就是系统调用,Linux API, 系统命令,和内核函数不是一回事,虽然他们有关联。
174+
系统调用是通过软中断向内核提交请求,获取内核服务的接口,Linux Api则定义了一组
175+
函数如read,malloc等,封装了系统调用, 比如malloc函数会调用brk系统调用。
176+
然后有系统命令则更高一级,如ls,hostname,则直接提供了一个可执行程序, 关于他们
177+
的关系可以阅读下面这篇文章:
178+
179+
http://wenku.baidu.com/view/9e33f3e94afe04a1b071de81.html
180+
181+
C语言要想使用别人的东西,首先要包含别人提供的头文件,使用linux api和c库函数
182+
也一样,默认的这些头文件都在/usr/include里,自己安装的一些则一般约定放在
183+
/usr/local/include里。写代码的过程中如果遇到一些类型或函数不知道怎么使用,直接
184+
可以在这里面找到头文件看源码。
185+
186+
Linux下还有好多数据类型是在学普通C语言是没见到过的,比如size_t,ssize_t,unit32_t
187+
啥的, 这些其实都在普通数据类型的别名,一般在/usr/include/asm/types.h里可以看到
188+
他们是怎么被typedef的,使用这些类型主要是为了提高可移植性,同时语义更加明确,
189+
比如size_t在32位机器上定义为uint,64位机器上定义为ulong,使用size_t编写的代码
190+
就可以在32位机器和64位机器上良好运行。 还有size_t的意义更明确,它不是用来表示
191+
普通的无符号数字概念你,而是表示sizeof返回的结果或者说是能访问的体系内存的长度。
192+
193+
然后像uint32_t这种类型是为了编写出更明确的代码,像C语言的类型,int, long等在
194+
不同的机器上都有不同的长度,但uint32_t在啥机器上都是32位长的,有时候需求就是
195+
这样,就需要用这种数据类型了。
196+
197+
还有就是Linux系统函数调用失败,大多数时候都会erron赋一个整数值,这个整数值可以
198+
表示不同的错误原因,可以在终端下运行man errno来查看详细,另外好多系统函数都可以
199+
用man来查看帮助的,有的里面还有使用示例的,是学习linux编程的很好的工具。
200+
201+
202+
还有一些系统函数设计的挺好,我总结了一些惯用法吧算是,自己设计函数也可以学习
203+
204+
第一个是通过指针参数来获取数据,因为好多函数的返回值是int类型,表示函数调用
205+
是否成功,以及错误码,而这个函数本身的任务还要返回一些实质的信息,这时候就可以
206+
通过参数来填充数据,让调用者拿到,比如accept函数的使用
207+
(简化后的伪代码,不能执行):
208+
209+
struct sockaddr_in client;
210+
if (accept(listenfd, &client) >= 0) {
211+
printf("%s\n", client);
212+
}
213+
214+
这样我们调用一次函数,既能知道有没有调用成功,成功的话又能拿到客户端的描述符,
215+
以及对端的网络地址。
216+
217+
第二个是C没有类和对象的概念,但也可以模拟出来类似的概念,比如网络编程,通过
218+
socket函数创建一个描述符,比如说是fd,其实这就相当于一个类的实例,一个对象了,
219+
然后调用read(fd),send(fd),close(fd)等函数来操作它,和面向对象里用fd.read(),
220+
fd.send(),fd.close()只是用法不同而已,所以写C是能用得到一些面向对象的思想的。
221+
222+
第三个是在Linux里好多东西可以用描述符来表示,比如文件,硬件端口,网络连接等,
223+
然后可以针对描述符调用read,write等操作,这个是个很好的抽象,可以使用很简单的
224+
几个接口来实现很强大的功能,在写自己的C软件时也可以借鉴这个思路。就是先建立一个
225+
概念,然后写很多的函数来操作这个概念,而不是建立很多的概念,大家记不住的。
226+
227+
第四个是,C其实没有太多的类型检查功能,表示复杂的数据都用struct表示,而不同的
228+
struct是可以强转的,所以可以用带标志的struct来表达类似面向对象多态的概念,如
229+
bind函数需要一个struct sockaddr的参数,但ipv4和ipv6的地址分别用
230+
struct sockaddr_in和struct sockaddr_in6表示,感觉就相当于struct sockaddr的两个
231+
子结构,这样bind函数就使用父结构struct sockaddr来同时支持ipv4和ipv6了。
232+
需要注意子结构和父结构的标志成员要放在最前面,这样子结构转成父结构时,父结构
233+
才能正确的读出标志,从而在具体使用时强转为合适的子结构。
234+
235+
236+
就这样了,Linux编程入门我知道的就这些,更多可看《Unix环境高级编程》
117237

118238
### socket基础
119239

120-
socket, connect, gethostbyname, getaddrinfo, send, recv
240+
先学一些socket客户端编程来熟悉socket编程吧, 要连接到远程主机,首要要
241+
有个远程主机的地址,一个远程主机的地址包含对方的IP和端口,有时候我们
242+
只知道对方的域名,所以首先要解析出IP来,好多书上都是用gethostbyname来解析域名
243+
的,但它过时了,不支持ipv6,而且参数不支持ip格式的字符串,返回的地址必须拷贝
244+
后才能使用,否则同线程再调用一次该函数那地址就变了,总之是一个过时的函数了。
245+
246+
现在比较国际范的函数是getaddrinfo,可以通过man查它的用法,
247+
248+
int getaddrinfo(const char *node, const char *service,
249+
const struct addrinfo *hints,
250+
struct addrinfo **res);
251+
252+
该函数同时支持 ipv4和v6,然后host支持域名也支持ip格式的字符串,hints用来设置
253+
查询的一些条件,result用来获取查询到的结果,他是一个指向指针的指针类型。
254+
255+
这相当也是一个惯用法了,一个参数用来指定调用需求,一个指针参数来获取返回数据。
256+
像select就是调用需求和返回数据都是一个参数来表示,像pool就是调用需求和返回
257+
用两个参数了,前一个是const,后一个是指针。具体使用示例如下:
258+
259+
struct addrinfo* get_addr(const char *host, const char *port){
260+
struct addrinfo hints; // 填充getaddrinfo参数
261+
struct addrinfo *result; // 存放getaddrinfo返回数据
262+
263+
memset(&hints, 0, sizeof(struct addrinfo));
264+
hints.ai_family = AF_UNSPEC;
265+
hints.ai_socktype = SOCK_STREAM;
266+
hints.ai_flags = 0;
267+
hints.ai_protocol = 0;
268+
269+
if(getaddrinfo(host, port, &hints, &result) != 0) {
270+
printf("getaddrinfo error");
271+
exit(1);
272+
}
273+
return result;
274+
}
275+
276+
对了,getaddrinfo返回的result指向的内存是系统分配的,用完了要调用
277+
freeaddrinfo去释放内存的。其实getaddrinfo的内部实现挺复杂的,调用了一堆ga开头
278+
的函数,而且struct addrinfo其实也蛮复杂的,里面有好多信息,但用好它是写出
279+
同时支持ipv4,ipv6网络程序的关键。
280+
281+
创建socket, 要熟悉下family,socktype,protocol等概念和取值,查man吧
282+
283+
int create_socket(const struct addrinfo * result) {
284+
int fd;
285+
286+
if ((fd = socket(result->ai_family, result->ai_socktype, result->ai_protocol)) == -1) {
287+
printf("create socket error:%d\n", fd);
288+
exit(-1);
289+
}
290+
printf("cerate socket ok: %d\n", fd);
291+
return fd;
292+
}
293+
294+
连接目标主机, 这里其实就是要三步握手了,有几个常见的错误,可以通过检测errno来
295+
读取,如ETIMEDOUT表示建立连接超时,就是发出去sync没人打理,或ECONNREFUSED表示
296+
对方端口没开,发过去的sync直接被对方发了个rst回来,或EHOSTUNREACH表示对方机器
297+
没开或宕机了,因为ICMP包返回错误了。
298+
299+
int connect_host(int fd, const struct addrinfo* addr) {
300+
if (connect(fd , addr->ai_addr, addr->ai_addrlen) == -1) {
301+
printf("connect error.\n");
302+
exit(-1);
303+
}
304+
printf("collect ok\n");
305+
return 0;
306+
}
307+
308+
我们要做一个HTTP客户端,类似curl,要拼一个HTTP请求发送给远程主机,拼包用
309+
snprintf虽然弱了一点,但也是最容易理解的,先用着。
310+
311+
int get_send_data(char * buf, size_t buf_size, const char* host) {
312+
const char *send_tpl; // 数据模板,%s是host占位符
313+
size_t to_send_size; // 要发送到数据大小
314+
315+
send_tpl = "GET / HTTP/1.1\r\n"
316+
"Host: %s\r\n"
317+
"Accept: */*\r\n"
318+
"\r\n\r\n";
319+
320+
// 格式化后的长度必须小于buf的大小,因为snprintf会在最后填个'\0'
321+
if (strlen(host) + strlen(send_tpl) - 2 >= buf_size) { // 2 = strlen("%s")
322+
printf("host too long.\n");
323+
exit(-1);
324+
}
325+
326+
to_send_size = snprintf(buf, buf_size, send_tpl, host);
327+
if (to_send_size < 0) {
328+
printf("snprintf error:%s.\n", to_send_size);
329+
exit(-2);
330+
}
331+
332+
return to_send_size;
333+
}
334+
335+
int send_data(int fd, const char *data, size_t size) {
336+
size_t sent_size;
337+
printf("will send:\n%s", data);
338+
sent_size = write(fd, data, size);
339+
if (sent_size < 0) {
340+
printf("send data error.\n");
341+
exit(-1);
342+
}else if(sent_size != size){
343+
printf("not all send.\n");
344+
exit(-2);
345+
}
346+
printf("send data ok.\n");
347+
return sent_size;
348+
}
349+
350+
完了收数据,我们只取HTTP应答第一行就好了,然后关闭连接。
351+
352+
int recv_data(int fd, char* buf, int size) {
353+
int i;
354+
int recv_size = read(fd, buf, size);
355+
if (recv_size < 0) {
356+
printf("recv data error:%d\n", (int)recv_size);
357+
exit(-1);
358+
}
359+
if (recv_size == 0) {
360+
printf("recv 0 size data.\n");
361+
exit(-2);
362+
}
363+
// 只取HTTP first line
364+
for (i = 0; i < size - 1; i++) {
365+
if (buf[i] == '\r' && buf[i+1] == '\n') {
366+
buf[i] = '\0';
367+
}
368+
}
369+
printf("recv data:%s\n", buf);
370+
}
371+
372+
int close_socket(int fd) {
373+
if(close(fd) < 0){
374+
printf("close socket errors\n");
375+
exit(-1);
376+
}
377+
printf("close socket ok\n");
378+
}
379+
380+
最后用main函数把他们串起来
381+
382+
int main(int argc, const char *argv[])
383+
{
384+
const char* host = argv[1]; // 目标主机
385+
char send_buff[SEND_BUF_SIZE]; // 发送缓冲区
386+
char recv_buf[RECV_BUFF_SIZE]; // 接收缓冲区
387+
size_t to_send_size = 0; // 要发送数据大小
388+
int client_fd; // 客户端socket
389+
struct addrinfo *addr; // 存放getaddrinfo返回数据
390+
391+
if (argc != 2) {
392+
printf("Usage:%s [host]\n", argv[0]);
393+
return 1;
394+
}
395+
396+
397+
addr = get_addr(host, "80");
398+
client_fd = create_socket(addr);
399+
connect_host(client_fd, addr);
400+
freeaddrinfo(addr);
401+
402+
to_send_size = get_send_data(send_buff, SEND_BUF_SIZE, host);
403+
send_data(client_fd, send_buff, to_send_size);
404+
405+
recv_data(client_fd, recv_buf, RECV_BUFF_SIZE);
406+
407+
close(client_fd);
408+
return 0;
409+
}
410+
411+

0 commit comments

Comments
 (0)