Socket

Socket的简单概念

sock的本质,可以理解为高级管道?,也是伪文件。

有两端,有两个,成对的,是封装好的。

也可以理解为实际上一个文件描述符指向两个管道文件的缓冲区,一个读一个写。文件描述符服务端一个,客户端一个,然后完成全双工通信

网络字节序

大端:低地址存高位,高地址存低位

小端:低地址存地位,高地址存高位

在计算机中一般数据结构存储都是小端存储法

网络字节流则是大端法存储。(历史遗留问题)

所以呢我们使用库函数做网络字节序和字节序进行转换

  • htonl
  • htons
  • ntohl
  • ntohs

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; /* 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 */
};

为啥 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 协议
    • 传值为0是默认协议
  • 成功返回文件描述符

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)

注意,这玩意不是监听,而是指定允许多少个客户端同时建立连接

,处于三次握手的队列和刚刚建立三次握手队列的链接数和。

  • 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);

客户端用,主动用来与服务端建立连接,都是传入参数,都挺简单

  • 传入一个服务端的addr
  • 大小

简单实现

话不多说直接上代码,比较简单的程序

服务端:

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;
//创建socket
lfd = socket(AF_INET, SOCK_STREAM, 0);
//指定ip协议族ipv4
serv_addr.sin_family = AF_INET;
//主机端口号,这里需要网络序列转换存储
serv_addr.sin_port = htons(SER_PORT);
//INADDR_ANY是与一个系统提供的宏,表示是一个有效的任意值,同养也需要存储转换
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定套接字地址数据
bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//设定默认连接队列上限128
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);
//bind可以不绑定,让系统帮你调用bind,客户端限定
//定义服务器的地址,地址长度
struct sockaddr_in serv_addr;
socklen_t serv_addrlen;
//初始化,方式结构体创建失败,良好习惯
memset(&serv_addr, 0, sizeof(serv_addr));
//协议族于服务端一致
serv_addr.sin_family = AF_INET;
//serv_addr.sin_addr.s_addr = htons(SER_IPV4);
//可以直接转换换成以下写法,转换成下面的写法,请求的地址不对
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
//write
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;
}

//read
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){
//Todo:判断信号错误
}
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){//大于0说明读一次还没读完
if((nread = read(fd, ptr, nleft)) < 0){
if(errno == EINTR){//被信号中断那就重新读取
nread = 0;
}else{//其他情况直接返回错误
printf("readn error");
return -1;
}
}else if(nread == 0){//等于零就说明读取完了
break;
}//大于零的情况,说明多读n次还没完,
nleft -= nread;//更新剩下字节的值
ptr += nread;//文件指针偏移。
}
return n - nleft;//返回实际读取的字节数;一般循环跳出后nleft其实是0;
}

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;
}