Linux下的socket编程

我发现c语言学习环境极为恶劣, 有问题上网找, 首先c关键词太弱..而且c语言的博客根本没法看, 一段代码基本是不能跑的, 如果有缩进我都已经感激涕零了. 不得不感叹还是js好啊, 毕竟会js的文章不会太丑.

所以我写c有关的学习博客, 都只求代码美观, 能直接运行.

socket

socket方法是用来获取一个文件描述符的。

int fd = socket(PF_INET, SOCK_STREAM, 0);

SOCK_STREAM表示流式数据,也就是tcp协议,udp的参数是SOCK_DGRAM.全程叫datagram,数据豌豆,也就是我们常说的数据报啦。

这下我终于明白为啥nodejs中使用udp协议是require("dgram");

AF_INETPF_INET

在网上找例子的时候, 会看到有的写AFINET, 有的写PFINET, AF是address family的缩写, PF是protocol family的缩写

这两个完全相同

AF_INET = 2
PF_INET = 2
AF_INET6 = 10
PF_INET6 = 10

他们的区别在于, AF是BSD的, 而PF是POSIX的.在windows上完全相同, 在我看来直接用2没什么问题.

bind

int bind(int fd, struct sockaddr *addr, socklen_t addrlen);

但我们一般不适用sockaddr, 因为sockaddr只有两个属性, 不能语义化的体现一个套接字地址.(ip, 协议, 端口)

因此我们使用sockaddr_in这个结构体代替, 这两个结构体长度相同, 可以直接强制转换(其实ipv6的sockaddr_in6长度不同依然是强制转换).

真正使用的时候一般是这样

int bind(fd, (struct sockaddr *)&addr, sizeof(addr));

sockaddr_in

一般使用sockaddr_in这个更加语义化的结构体来表示ipv4的套接字地址, ipv6为sockaddr_in6,需要引用netinet/in.h.

struct sockaddr_in {
    sa_family_t sin_family;
    in_port_t sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8];
}

这么多sin是啥意思, 我猜可能是socket inet的缩写.

sin_family_t就是前面的unsigned short类型的PF_INET, 也就是2.

sin_port当然就是端口, 但不能直接写, 需要用例如honts(80);转化.

sin_addr.s_addr同样需要转换,addr.sin_addr.s_addr = inet_addr("0.0.0.0");

sin_zero仅仅是用来对齐sockaddr大小的。

accept

accept函数是接到请求的时触发的,一般网络出错调试,都可以在accept上设置断点,然后逐步调试,但要注意的是accept并不是进行3步握手, 3步握手是更加底层的系统完成的,accept只是把完成握手的请求从系统中拿出来。

accept是放在while 1中的,这样它永远尝试着去获取新的socket文件描述符。

int len = sizeof(addr);
while (1) {
  req_fd = accept(fd, (struct sockaddr *)&addr, &len);
  ...
}

第二个参数类型是sockaddr, 存在sockaddr_in中,这样方便我们读取其ip和端口信息

第三个参数居然是地址,以我的水平是无法理解的。

accept的任务是获取一个新的请求fd。于是我们就可以通过这个新的fd进行接收数据和发送数据了。

接收数据 recv

char buf[1000];
recv(req_fd, buff, sizeof(buf), 0);

这函数api很清楚,从req_fd中读数据,放入buf中,buf长度为参数3.

那如果请求头太长怎么办呢?比如一个GET的路径就1k大了。这时候如果长度超出会怎样呢?我们可以做一个小实验。

int req_fd;
int size, size2;
char[300] buf;
char[300] buf2;

while (1) {
  if ((req_fd = accept(fd, addr, &len) == -1) {
    exit(1);
  }
  size = recv(req_fd, buf, 300, 0);
  size2 = recv(req_fd, buf, 300, 0);
}

这时候我们发现size等于300, size2等于133.(这是使用中文chrome请求时的头部),可以看出,size2还没装满300,说明缓冲区已经被我们拿完了,那我们继续recv会怎样呢?我尝试了一下,程序直接没反应了,也没有报错。

发送数据 send

char res_str[] = "HTTP/1.1 200 OK\nContent-Type: text/html ...";
send(req_fd, res_str, sizeof(res_str), 0);

可以看到send和recv确实是一对api,参数基本一样,第四个参数都可以不管直接写0