Socket Socket的简单概念 sock的本质,可以理解为高级管道?,也是伪文件。
有两端,有两个,成对的,是封装好的。
也可以理解为实际上一个文件描述符指向两个管道文件的缓冲区,一个读一个写。文件描述符服务端一个,客户端一个,然后完成全双工通信
网络字节序 大端:低地址存高位,高地址存低位
小端:低地址存地位,高地址存高位
在计算机中一般数据结构存储都是小端存储法
网络字节流则是大端法存储。(历史遗留问题)
所以呢我们使用库函数做网络字节序和字节序进行转换
n是net网络
h是host主机
l是长整32位数,ipv4用
s是短整16位数,port用
ip地址转换函数 前置函数
都支持ipv4 ipv6
inet_ntop
ip转二进制
inet_pton
二进制转ip
sockaddr的数据结构 原始的已经废弃了:struct sockaddr,毕竟一开始只是为了ipv4的,没想着拓展,用着用着不好用,换了其他新的。 如此就会出现历史遗留问题比如说使用如下函数:bind
accept
connect
传值的类型需要进行强转
现在是struct sockaddr_in(搞不懂为啥不直接代替,而是加了一个in)
1 2 3 4 5 6 7 8 9 10 struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; }; struct in_addr { uint32_t s_addr; };
为啥 in_addr 这个结构体只有一个成员,估计以前遗留的被删了,要么就是为了扩展。
socket函数 socket 1 int socket (int domain, int type, int protocol)
domain
常用的三个宏
AF_INET ipv4
AF_INET6 ipv6
AF_UNIX 本机传输linux下
type
SOCK_STREAM 流 (tcp)
SOCK_DGRM 数据报(udp)
protocol 协议
成功返回文件描述符
bind 1 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) )
bind 绑定的意思,将网络参数,地址绑定到创建的socket 文件上
sockfd 文件描述符
addr 结构体sockaddr* 需要注意要进行类型强转哦
addrlen addr的长度
listen 1 int listen (int sockfd, int backlog)
注意,这玩意不是监听,而是指定允许多少个客户端同时建立连接
,处于三次握手的队列和刚刚建立三次握手队列的链接数和。
accept 1 int accept (int sockfd, struct sockaddr* addr, socklen_t *addrlen)
乍一看跟bind的参数是一样的,但是注意:
第二个参数是传参参数:传出客户端的ip地址
第三个比较特别,这是一个传入传出的参数,先读一次再写一次
返回值 成功返回一个 新的 socket的文件描述符,这个文件描述符,用来与客户端的服务端之间的通信
1 2 3 4 RETURN VALUE On success, these system calls return a file descriptor for the ac‐ cepted socket (a nonnegative integer). On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
man文档中也有表明
connect 建立连接
1 int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
客户端用,主动用来与服务端建立连接,都是传入参数,都挺简单
简单实现 话不多说直接上代码,比较简单的程序
服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <ctype.h> #define SER_PORT 6666 #define SER_IPV4 "127.0.0.1" int main () { int lfd; struct sockaddr_in serv_addr ; lfd = socket(AF_INET, SOCK_STREAM, 0 ); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SER_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); bind(lfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); listen(lfd, 128 ); struct sockaddr_in client_addr ; socklen_t clinet_addrlen; clinet_addrlen = sizeof (client_addr); int cfd = accept(lfd, (struct sockaddr *)&client_addr, &clinet_addrlen); char buf[BUFSIZ]; for (;;) { int n = read(cfd, buf, BUFSIZ); for (int i = 0 ; i < n; i++){ buf[i] = toupper (buf[i]); } } write(cfd, buf, n); close(lfd); close(cfd); return 0 ; }
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <stdlib.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdio.h> #define SER_PORT 6666 #define SER_IPV4 "127.0.0.1" int main () { int cfd; cfd = socket(AF_INET, SOCK_STREAM, 0 ); struct sockaddr_in serv_addr ; socklen_t serv_addrlen; memset (&serv_addr, 0 , sizeof (serv_addr)); serv_addr.sin_family = AF_INET; inet_pton(AF_INET, SER_IPV4, &serv_addr.sin_addr.s_addr); serv_addr.sin_port = htons(SER_PORT); char buf[BUFSIZ]; int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); if (ret < 0 ){ perror("socket" ); exit (1 ); } for (;;){ fgets(buf, BUFSIZ, stdin ); write(cfd, buf, strlen (buf)); int n = read(cfd, buf, BUFSIZ); write(STDOUT_FILENO, buf, n); } close(cfd); return 0 ; }
以上调用的函数基本上错误都给省了没有实现,所以会有一些,意料之外的结果。
在IO 函数中,曾提了一两嘴关于read 和write在操作网络字节流的一些需要注意的点。
手动修改write 和read 函数调用方式,能够避免出现,数据一次没读全需要分包的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ssize_t write_len = 0 ;ssize_t buflen = strlen (buf) ;while (buflen> wirte_len){ ssize_t retwrite = write(cfd, buf+ write_len,buflen - write_len); if (retwrite =< 0 ){ close(cfd); } wirte_len += retwrite; } ssize_t read_len = 0 ;ssize_t buflen = strlen (buf) ;while (buflen> read_len){ ssize_t retread = read(cfd, buf+ read_len,buflen - read_len); if (retread < 0 ){ close(cfd); } if (retread == 0 ){ } read_len += retread; }
如果你说,一般不会出现错误,没有特别需求就不进行检查吧
不怕一万就怕万一,一个健壮的代码,需要考虑所有可能的情况。
比如说端口号在调用结束后会有一段时间的time_wait的状态,再执行服务器,实际上是无法bind绑定端口的。
实际上如果想偷懒,这里可以做一个错误函数封装。
封装一下容易出错的函数。这个出错函数并非只能在一次项目中使用,在别的项目中,一样可以调出来使用。
这里就不全部放出来了。我封装的东西还蛮多的。至于为什么直接封装成首字母大小的原因,就是比较方便查看man文档
readn 由于socket的read write 操作不同一般的文件的io操作
socket上文件读写常会比函数调用指定的字节少
(网络波动之类的原因,比如丢包什么的。或者写端根本没这么多字节的等等),
再判断的时候如果 只是小于0就认为错误是比较草率的。有些并非是错误的情况,依然会被杀死。
还有的可能就是socket的缓冲区已经到达了极限容量,无法执行读写操作,或者被信号中断。
遇到这种情况,就必须再次调用read以写入或输出剩余的字符,
所以就需要readn(可以理解为read调用n次)
根据网络UNIX 网络编程卷一中,提供了readn 和writen的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ssize_t Readn (int fd, void *vptr, size_t n) { size_t nleft; ssize_t nread; char *ptr; ptr = vptr; nleft = n; while (nleft > 0 ){ if ((nread = read(fd, ptr, nleft)) < 0 ){ if (errno == EINTR){ nread = 0 ; }else { printf ("readn error" ); return -1 ; } }else if (nread == 0 ){ break ; } nleft -= nread; ptr += nread; } return n - nleft; }
writen 实现思想与readn一样。这里就不进行解释了。直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ssize_t Writen (int fd, void *ptr, size_t n) { int nleft; int nwritten; nleft = n; while (nleft > 0 ){ nwritten == write(fd, ptr, nleft); if (nwritten <= 0 ){ return nwritten; } nleft -= nwritten; ptr += nwritten; } return n - nleft; }