Unix网络编程之基础函数(一)

TCP客户端与服务器之间交互过程在程序实现上的体现如下图所示,注意思考这个过程中TCP状态的变化(参考TCP状态转换图):

服务器首先调用socket函数创建socket,通过bind函数将socket与主机地址及端口绑定,然后调用listen函数对主机端口进行监听,然后调用accept函数接收客户端发来的请求,若没有客户端请求,服务端将阻塞在此。客户端建立socket,然后调用connect函数与服务器三次握手建立连接,发送请求给服务器,服务器接收到请求之后对其进行处理并将处理结果返回给客户端,这个过程一直持续下去,一直到客户端与服务器断开连接,服务器接下来将关闭连接或继续等待下一个客户端的连接。

socket函数

为了执行网络IO,无论是客户端还是服务器,一个进程首先要做的事情就是调用socket函数创建一个socket,并指定socket的协议族,类型以及协议。

函数原型

1
2
3
#include <sys/socket.h>
int socket(int family, int type, int protocol);

参数说明

  • family : 协议族,对应于sockaddr_in中的sin_family,IPv4协议时值为AF_INET.
  • type : 套接字类型,套接字类型有多种,TCP一般使用SOCK_STREAM,UDP使用SOCK_DGRAM
    • SOCK_STREAM : 字节流套接字,是一种有序可靠双向的面向连接字节流的套接字
    • SOCK_DGRAM : 数据报套接字,是一种长度固定,无连接的不可靠的套接字。
    • SOCK_SEQPACKET : 有序分组套接字,是一种长度固定,有序,可靠的面向连接的有序分组套接字。
    • SOCK_RAW : 原始套接字
  • protocal : 链接层传输协议,包括TCP,UDP,SCTP传输协议
    • IPPROTO_TCP : TCP传输协议
    • IPPROTO_UDP : UDP传输协议
    • IPPROTO_SCTP : SCTP传输协议

一般情况下,我们在使用socket函数的时候,可以只指定前两个参数,将第三个参数设为0,这个时候socket会将protocol设置为默认值,如当family=AF_INET,type=SOCK_STREAM时,这个时候默认的协议为IPv4 TCP协议。但是,注意有些组合是不可用的,组合如下:

AF_TNET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP/SCTP TCP/SCTP
SOCK_DGRAM UDP UDP
SOCK_SEQPACKET SCTP SCTP
SOCK_RAW IPv4 IPv6

返回值说明

socket函数与open函数类似,若成功,返回一个较小的非负整数,称为套接字描述符,若失败返回-1;

connect函数

TCP客户端通过connect函数来与服务器建立连接,注意客户在调用connect函数之前不必调用bind函数将sockfd与本机的IP端口绑定,因为如果需要的话,内核会确定源IP地址,并选择一个可用的临时端口作为端口号

客户端调用connect函数的时候会激发三次握手连接建立。而且只在连接建立成功或失败时返回,否则会一直阻塞在connect函数处。

connect函数出错的情况包括以下几种:

  • 若TCP在调用connect之后没有接收到syn分节,则返回ETIMEOUT错误。这种情况通常发生在目的主机不存在情况下。这时客户端会隔一段时间发送一次请求,若等待时间超过一定时间(这个过程客户端被阻塞),返回ETIMEOUT错误。
  • 若T客户端收到RST分节(复位),则返回ECONNREFUSED错误,这种情况发生在目的主机存在,但是主机上没有进程监听指定端口。
  • 若客户端发送的分节在某个路由器上返回destination unreachable,此时返回EHOSTUNREACH,这种情况发生在目的主机和端口存在,但是中间路由出现问题

从TCP状态图可以看出,connect函数可以使得socket状态从closed转移到SYN_SENT状态,若成功,转换到ESTABLISHED状态。若套接字失败不可用,必须关闭。然后重新调用socket函数。

函数原型

1
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);

参数说明

  • sockfd : 此参数为调用sock函数的返回值,即套接字描述符
  • servaddr : 指向服务器套接字地址结构的指针,套接字地址结构中必须含有Ip地址以及端口。
  • addrlen : 上述套接字的长度。

返回值说明

连接失败,返回-1,并将错误码写入errno中;
若连接成功,则返回0.

bind函数

在建立socket之后,我们通常会将socket与一个套接字绑定,即将一个协议地址赋予给socket,bind函数的作用就是将socket与socket地址绑定。socket协议地址是32位的IPv4地址或128位的IPv6和16位的TCP或UDP端口的组合。

一般情况下,客户端一般不调用bind函数进行绑定socket地址(可调用),当其调用connect函数的时候,内核会获取主机的IP作为源IP,并选择一个可用的端口作为源端口,当然,客户端可以调用bind为socket指定源端口和IP;服务端一般要调用bind端口,为socket指定监听的端口和IP,因为一般情况下服务器是对外提供服务的,如http的80端口,https的443端口等等。

注意:socket绑定的端口必须是进程所在主机上的网络接口之一,不能是其他主机的IP,否则会出现“Cannot assign requested address”,也不能绑定已经在使用的端口。

函数原型

1
2
3
#include <sys/socket.h>
//函数参数与connect函数中的参数及意义相同。
int bind(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);

bind函数可以只绑定IP,只绑定端口,也可以两者都绑定,也可以两者都不绑定,具体的制定规则如下:

IP地址 端口 说明
统配地址(INAAR_ANY) 0 内核选择IP地址和端口
统配地址(INADDR_ANY) 非0 内核选择IP地址,进程指定地址
本地IP地址 0 进程指定IP,内核选择端口
本地IP地址 0 进程指定IP和端口

一般情况下,INADDR_ANY的值为0,将其转化为s_addr的时候使用htonl函数(将主机字节序转化为网络字节序),若是IPv6则不能使用htonl,因为IPv6的地址是一个128位的地址。需要使用另一种方式,具体参考《Unix网络编程卷一第三版》83页。

若设置为内核选定端口,则必须调用getsockname函数获取系统选定的端口。
从bind函数返回的一个常见的错误是:EADDRINUSE : address already in use;

listen函数

函数原型

1
2
#include <sys/socket.h>
int listen(int sockfd, int backlog);

listen函数仅由TCP服务器调用,一般在调用socket和bind函数之后调用 .当进程调用socket函数创建套接字之后,这个套接字被默认假设为主动套接字,(主动套接字的意思是将会调用connect去和TCP服务器建立连接)。listen函数主要有以下两个作用:

  • 将一个未连接的套接字转化为主动套接字,指示内核应该接收指向该套接字的连接请求。调用listen成功之后TCP从CLOSED状态转变为LISTEN状态。
  • 指定内核应该为相应套接字排队的最大连接数。即backlog指定,下面两个队列的和不能超过backlog.若查过backlog还有连接请求,服务器将直接发送RST复位拒绝连接。一般不要吧backlog值设为0,因为不同系统实现对0的解释不同;一般讲backlog的值设为5,因为这是4.2BSD支持的最大值。

内核为任何一个给定的监听套接字维护两个队列:

  • 未完成连接队列 :当服务器收到客户端发来的请求之后,数据还没有处理完,此时会进入SYN_RECV状态,这个时候将进入未完成连接队列,也叫SYN队列
  • 已完成连接队列:服务器与客户端之间建立连接之后,进入ESTABLISHED状态,此时进入已完成连接队列,也叫accept队列。

accept函数

accept函数的作用是从已经完成连接的队列头返回下一个已经完成的连接,如果已完成的连接为空,则进程进入睡眠状态(阻塞)。

函数原型

1
2
#include <sys/socket.h>
int accept(int servsockfd, struct sockaddr* cliaddr, socklen_t* addr_len);

参数说明

  • servsockfd : 监听的套接字描述符
  • cliaddr : 客户端的sockaddr,如果对客户端的来源不感兴趣,可以将其设置为NULL;
  • addr_len : aliaddr的字节数。即客户端sockaddr的地址长度。

返回值说明

若出错返回-1;如果accept成功,返回一个新的socket描述符。其代表与所返回的客户端之间的TCP连接,它是一个连接套接字,之后服务端接收和发送数据都将使用这个描述符进行操作.

Invalid argument常见原因

在accept的时候,我们常会遇到Invalid argument的错误,出现这个错误的原因有很多,主要是在accept之前的准备工作出了问题。

  • socket创建不成功,在socket函数后检查一下errno值
  • 绑定socket地址不成功,检查一下返回值,同时检查一下errno
  • 没有listen,或者listen出问题。

以上三步有一步出问题或者漏掉一步都有可能出现Invalid argument错误。

close函数

我们知道,关闭文件描述符我可使用close函数,同样的close函数也能用来关闭socket文件描述符。

1
2
#include <unistd.h>
int close(int sockfd);

通常情况下,close一个TCP套接字只是将该socket描述符标记为不可用,这个时候其将不能作为read和write的第一个参数进行数据的发送和接收,然而TCP尝试发送已经排队等待的所有需要发送到对端的数据,发送完毕之后将执行正常的TCP连接终止流程关闭TCP连接.

Comment and share

Unix网络编程之主机字节序与网络字节序

在各种计算机体系中,对于字节,字等的存储机制有所不同,但是在网络通信过程中,如果双方交流的信息存储结构不一致,则会导致通信失败的结果。当前计算机中通常采用的字节存储机制主要有两种:大端规则与小端规则网络通信的过程中的存储机制统一为大端规则。

字节序

参考:http://www.cppblog.com/tx7do/archive/2015/12/14/71276.html

Comment and share

Unix网络编程基础之套接字结构

大多数的套接字函数都使用到了套接字地址,它们以套接字地址的指针作为参数。每个协议族都定义了自己的套接字地址结构,这些套接字地址结构均以sockaddr_开头,以协议族唯一的后缀结尾。

IPv4套接字地址结构

IPv4的套接字以sockaddr_in命名,其具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in {
uint8 sin_len; //套接字的长度,sizeof(struct sockaddr_in)
sa_family_t sin_family; //协议族
in_port_t sin_port; //套接字端口
struct in_addr sin_addr; //套接字地址
char sin_zero[8]; //保留位
}

  • sin_len : 套接字的长度字段,类型为uint8_t,sizeof(struct sockaddr_in),不是所有的系统都支持。长度字段简化了可变长度的套接字的处理。在使用过程中无需设置和检查它,除非涉及路由套接字。
  • sin_family : 协议族,IPv4的协议族为AF_INET.类型为无符号整形,其长度受系统的影响。若sockaddr_in中含有sin_len字段,其大小为16位,若含有长度字段,其大小为8位。
  • sin_port : 套接字端口, 一般为uint16_t类型。
  • sin_addr : 套接字Ip,其类型为in_addr,in_addr中的s_addr类型为uint32_t.
  • sin_zero : 不常用,若需要在套接字中加入额外字段,使用到此字段,若不使用将其置为0,一般使用sockaddr_in首先将整个结构置0

注意:套接字地址结构仅在本机上使用,虽然结构中的某些字段用在不同主机之间的通信,但是结构体本身不在主机之间传递。

协议族参数说明

网络通信过程中有不同的协议族,通常我们在socket地址的sin_family中指出当前通信使用的协议族,不同协议族对应不同的参数,其对应参数如下所示:

sin_family 协议说明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字协议
AF_KEY 密钥套接字

通用套接字地址结构

套接字函数以套接字地址结构指针作为参数的过程中,由于在C中没有继承的机制,这个时候向套接字函数传递参数的时候,由于不同协议的套接字地址不同,会出现问题。这个时候有一种解决办法就是传递void*指针给socket函数,但是void空指针的出现在socket函数之后,所以这个方案不可行。这个时候的解决方案是 定义一个通用的套接字函数,socket函数的参数为通用套接字地址的指针,传递参数的时候我们将特定的套接字指针强制转换为通用套接字地址指针。 如bind函数的函数原型为:

1
int bind(int, struct sockaddr*, socklen_t);

通用套接字地址的定义如下:

1
2
3
4
5
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};

Ubuntu16.04中sockaddr_in的定义

Ubuntu16.04中sockaddr_in的定义在/usr/include/netinet/in.h,注意其不支持sin_len字段,为了保持与通用套接字字符串兼容,其保留字符串的长度直接用通用套接字的大小减去其他字段。

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; // Port number.
struct in_addr sin_addr; // Internet address.
//Pad to size of struct sockaddr
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};

Comment and share

Unix时间相关函数总结

在进行Unix编程的过程中,我们不可避免的会遇到需要时间相关的操作,如文件的创建修改时间,数据库中字段插入或更新的时间。

Unix时间相关的类型

  • time_t
  • struct timeb
  • struct timeval
  • struct timespec
  • struct tm
  • clock_t

time_t

time_t是一个有符号的整数类型,表示的含义是从1970年1月1日到某一个时间点的秒数。若为32位系统,由int类型的范围可以推算出,time_t可以表示的时间范围是1901-12-13 20:45:52到2038-01-19 03:14:07。

struct ime_b

time_b结构体是一个精确到毫秒的结构体,其有四个成员,成员列表如下:

1
2
3
4
5
6
struct timeb{
time_t time;
unsigned short millitm;
short timezone;    //时区标志
short dstflag;     //夏令时标志
};

可以通过下列函数获取当前的timeb:

1
int ftime(struct timeb* tb);

struct timeval

timeval是一个精确到微妙的结构体。其中主要包含两个成员:

1
2
3
4
struct timeval{
time_t tv_sec;
suseconds tv_usec;
};

此值通常通过gettimeofday获取

1
2
#include <sys/time.h>
int gettimeofday(struct timeval* tv, struct timezone* tz); //timezone参数已废弃,一般设为NULL,

struct timespec

timespec是一个精确到纳秒的结构体。其主要包含两个成员

1
2
3
4
struct timespec{
time_t tv_sec; //秒
long tv_nsec; //纳秒
}

此结构体一般通过下列函数获取:

1
2
#include <sys/time.h>
long clock_gettime(clockid_t which_clock, struct timespec* tp);

上述参数中which_clock用于标识那种时钟时间,可选值如下

  • CLOCK_REALTIME       : 系统当前时间,1970-1-1开始
  • CLOCK_MONOTONIC      : 系统的启动时间,不能被设置
  • CLOCK_PROCESS_CPUTIME_ID : 进程运行时间
  • CLOCK_THREAD_CPUITME_ID : 线程运行时间
  • CLOCK_REALTIME_HR : CLOCK_REALTIME的高精度版本
  • CLOCK_MONOTONIC_HR : CLOCK_MONOTONIC的高精度版本

struct tm

struct tm被称为一种分解时间,日期和时间被分解成多个独立字段。其形式如下:

1
2
3
4
5
6
7
8
9
10
11
struct tm {
int tm_sec; //秒 (0-60)
int tm_min; //分 (0-59)
int tm_hour; //时 (0-23)
int tm_mday; //日 (1-31)
int tm_mon; //月 (0-11)
int tm_year; //年 (1900-)
int tm_wday; //一周中的周几(周日为0)
int tm_yday; //一年中的第几天。(1月1号为0)
int m_isdst; //DST大于0表示为夏令时时间。
}

Unix时间相关函数

Unix时间相关的函数除了上面已经提到的函数还包括以下函数

  • time(time_t timep)
  • ctime
  • gmtime
  • localtime
  • mktime
  • asctime
  • strftime
  • strptime

time

函数原型

1
2
#include <time.h>
time_t time(time_t* timep);

time函数返回当前时间的时间戳,此时间戳为从1970年1月1日到当前时间的时间戳,此值不受时区和夏令时(DST)。此函数的返回值为当前的时间戳,函数参数为time_t指针,当前时间除了返回值之外,还将此时间放入timep中。所以使用time函数时我们将timep设置为NULL即可。

ctime

函数原型

1
2
#include <time.h>
char* ctime(const time_t* timep);

函数功能

ctime函数的功能为将time_t转化为打印字符串格式。把一个指向time_t的指针timep传入函数ctime,将返回一个长度为26字节的字符串,包含\0和换行符。ctime进行转换的时候,会考虑时区和夏令时,所以返回的时间字符串为当地时间。返回时间的格式如下:

1
Wed Jun 8 14:22:34 2011

特别注意返回的字符串是经由静态分配的,若多次调用此函数,之前获取的时间会受影响。SuSv3规定,调用ctime(),gmtime(),localtime()以及asctime()中的任意一个函数,都可能覆盖其他函数返回的结果。

静态分配的意思是这些函数返回的数据都是般存在同一个静态变量中,所以下一次的结果会覆盖上一次的数据。如果需要对之前的数据保存,此时需要将结果拷贝到自己分配的内存中。

### gmtime和localtime

函数原型

1
2
3
#include <time.h>
struct tm* gmtime(time_t* timep);
struct tm* localtime(time* timep);

函数功能

gmttime和localtime的作用是将time_t值转换为分解时间struct tm类型。他们的主要区别是gmtime转换为对应于UTC的分解时间,而localtime考虑时区和夏令时

mktime

函数原型

1
2
#include <time.h>
time_t mktime(struct tm* timeptr);

#### 函数功能
mktime的作用是将一个本地时区的分解时间转化为time_t类型。需要注意的是,mktime可能改变timeptr所指的内容。若分解时间不符合要求,mktime将其自动转换为有效时间。如秒为61,此时会将其变成1并讲分加1。

asctime

函数原型

1
2
3
#incude <time.h>
char* asctime(const struct tm* timeptr);

函数功能

asc的功能是将分解时间转化为打印时间,特别注意的是asctime转化的过程中不考虑时区和夏令时,返回的数据也是静态分配的。

strftime

函数原型

1
2
#include <time.h>
size_t strftime(char* outstr, size_t maxsize, const char* format, const struct tm* timeptr);

函数功能

此函数的功能是将分解时间转换为打印时间,并可以指定打印时间的格式为format。不同于ctime和asctime,strftime结果不包含换行符。若返回的字符串超过了maxsize大小,函数返回0指示为转换错误。定义的格式中其格式化字符串可以参考预定义的格式。参考《Linux/Unix系统编程手册.上册》第158页。

Comment and share

C语言宏定义相关

C语言宏定义在代码编写中很常见,它常会带来一些很高的性能和很方便的写法,在看Linux源码中sockaddr_in的时候遇到宏定义中##。特地在此记录.

宏定义中##用法

问题背景

Linux中sockaddr_in的定义在文件/netinet/in.h文件中。具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; // Port number.
struct in_addr sin_addr; // Internet address.
// Pad to size of struct sockaddr.
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
#define __SOCKADDR_COMMON(sa_prefix)\
sa_family_t sa_prefix##family

从上面可以看出,__SOCKADDR_COMMON的宏定义中出现了##的使用方法。那它在宏定义中的意思是什么呢?

##详解

##是一种分隔连接方式。它的作用是先分隔,然后进行强制连接。

在普通的宏定义中,预处理器一般吧空格解释为分段标志,然后进行相应的替换工作。但是这样做的结果是被替换的段之间会出现空格。如果我们不希望这些空格出现,可以使用##来代替空格。

如:

1
2
#define type1(type,name) type name_##type##_type
#define type2(type,name) type name##_##type##_type

上述type1(int,c)将被替换为:int name_int_type
上述type2(int,c)将被替换为:int c_int_type

故我们再回去看SOCKADDR_COMMON的宏定义. SOCKADDRCOMMON (sin);将被解释为sa_family_t sin_family;

注意在函数参数中使用##可以将空字符串过滤掉,否则会出现问题。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define comac_get_args_cnt( ... ) comac_arg_n( __VA_ARGS__ )
#define comac_arg_n( _0,_1,_2,_3,_4,_5,_6,_7,N,...) N
#define comac_args_seqs() 7,6,5,4,3,2,1,0
#define comac_join_1( x,y ) x##y
//计算可变参数...中参数的个数.注意由于seq的范围为7-0,该宏可识别的参数个数的范围为0-7个。超过7个参数识别有问题
#define comac_argc( ... ) comac_get_args_cnt( 0,##__VA_ARGS__,comac_args_seqs() )
int main() {
//若上述comac_get_cnt中的__VA_ARGS__去掉##,结果为1,加上为0。
comac_argc();
}

#详解

在宏定义中,#符号的作用是将宏定义参数用””括起来,

1
2
#define example(test) printf("%s", #test);
example(123 456); //printf("%s", "123 456");

宏定义中占位符理解

在宏定义中我们可以使用字母或者单词作为占位符,初次之外,我们也可以使用_1,_2作为占位符,不要被它蒙骗了,其作用与单词作为占位符相同。如:

1
2
#define sum(a,b) (a+b)
#define sum(_1,_2) (_1+_2)//作用同上

Comment and share

protobuffer中string和bytes类型

从上一节protobuffer的介绍中我们知道字符串类型在protobuffer中有string和bytes两种类型,那这两种类型有什么区别呢,什么时候用string,什么时候用bytes。在C++中两种类型分别对应的是什么类型.下面将揭开迷雾

string与bytes区别

按照经验我们知道bytes一般适用于存储二进制数据的,但在C++中,string既可以存储ASCII文本字符串,也能存储任意多个\0的二进制序列,那两者的区别在哪里呢?

  • string类型(protobuffer中的string,与C++区别开)不能存储非法的UTF-8字符,如果遇到该字符,序列化的时候将会出错。

[libprotobuf ERROR google/protobuf/wire_format.cc:1091] String field ‘str’ contains invalid UTF-8 data when serializing a protocol buffer. Use the ‘bytes’ type if you intend to send raw bytes.

###出现上述错误的原因
这里从ProtoBuf的源码进行分析。protoBuf在序列化的过程中,都会调用SerializeFieldWithCachedSizes这个函数。我们看一下序列化string和bytes在序列化过程中的区别。

对于string类型:

1
2
3
4
5
6
7
8
9
10
11
case FieldDescriptor::TYPE_STRING: {
string scratch;
const string& value = field->is_repeated() ?
message_reflection->GetRepeatedStringReference(
message, field, j, &scratch) :
message_reflection->GetStringReference(message, field, &scratch);
VerifyUTF8StringNamedField(value.data(), value.length(), SERIALIZE,
field->name().c_str());
WireFormatLite::WriteString(field->number(), value, output);
break;
}

对于bytes类型:

1
2
3
4
5
6
7
8
9
case FieldDescriptor::TYPE_BYTES: {
string scratch;
const string& value = field->is_repeated() ?
message_reflection->GetRepeatedStringReference(
message, field, j, &scratch) :
message_reflection->GetStringReference(message, field, &scratch);
WireFormatLite::WriteBytes(field->number(), value, output);
break;
}

从上面可以看到,序列化string和bytes的区别主要在于:string类型序列化调用了VerifyUTF8StringNamedField函数检验string中是否有非法的UTF-8字符。其中VerifyUTF8StringNamedField实现如下:

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
void WireFormat::VerifyUTF8StringFallback(const char* data,
int size,
Operation op,
const char* field_name) {
if (!IsStructurallyValidUTF8(data, size)) {
const char* operation_str = NULL;
switch (op) {
case PARSE:
operation_str = "parsing";
break;
case SERIALIZE:
operation_str = "serializing";
break;
// no default case: have the compiler warn if a case is not covered.
}
string quoted_field_name = "";
if (field_name != NULL) {
quoted_field_name = StringPrintf(" '%s'", field_name);
}
// no space below to avoid double space when the field name is missing.
GOOGLE_LOG(ERROR) << "String field" << quoted_field_name << " contains invalid "
<< "UTF-8 data when " << operation_str << " a protocol "
<< "buffer. Use the 'bytes' type if you intend to send raw "
<< "bytes. ";
}
}

string和bytes类型在C++和Java中的区别

protobuf类型在C++和java中的类型对应如下:

  • 在C++中,string和bytes的实现都是std::string类型。
  • 在Java中string和bytes类型的实现分别是String和ByteString。

为什么bytes类型可以描述string类型,还需要string呢?

根据论坛上说的,string类型在Java中有较多的API可供使用,而bytes较少,所以能定义为string的尽量定义为string,如果字段值确定或者可能含有非法的utf-8编码,则使用bytes类型。

Comment and share

Linux Regex正则表达式库

标准的C/C++库不支持正则表达式。在Posix函数库中包含了正则表达式库。

## 正则表达式匹配框架

标准的正则表达式匹配框架:

  • 编译正则表达式.
  • 匹配正则表达式.
  • 释放正则表达式.

编译正则表达式

1
2
#include <regex.h>
int Regcomp(regex_t* preg, const char* regex, int cflags);

参数说明

  • preg : 用来保存编译之后的结果
  • regex : 正则表达式字符串,表示被编译的正则表达式。
  • cflags : 编译控制参数
    • REG_EXTENDED : 使用扩展正则表达式模式
    • REG_ICASE : 对规则中字符串不区分大小写
    • REG_NOSUB : 只检查是否有符合规则的子串。

返回值

编译成功返回0,否则返回非0

匹配正则表达式

1
2
3
4
5
6
7
8
9
10
typedef struct {
regoff_t rm_so;
regoff_t rm_eo;
} regmatch_t;
int regexec(const regex_t* preg,
const char* string,
size_t nmatch,
regmatch_t pmatch[],
int eflags);

参数说明

  • preg : 上述编译之后的正则表达式regex_t指针。
  • string : 被匹配的字符串。
  • match : 被匹配的个数。告诉函数regexec最多可以把多少个匹配结果写入pmatch,一般为pmatch数组的长度。
  • pmatch : 匹配结果数组。
    • rm_so : 满足子串在string中的起始偏移量
    • rm_eo : 满足子串在string中的结束偏移量
  • eflags : 匹配的特性
    • REG_NOTBOL : 是否为第一行
    • REG_NOTEOL : 是否是最后一行

返回值

0表示匹配成功,1表示REG_NOMATCH。

报错信息

1
size_t regerror(int errcode, const regex_t* preg, char* buf, size_t buffer_size);

参数说明

  • errcode : 来自regcomp和regexec的错误码。
  • preg : 编译后的正则表达式
  • buf : 缓冲区错误信息字符串
  • buffer_size : 缓冲区最大长度。

释放正则表达式

1
2
//释放reget_t指针,无返回值。
void regfree(reget_t* preg);

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <regex.h>
#include <stdlib.h>
int main() {
regex_t preg;
int iErr = 0;
regmatch_t subs[256];
char acReg[] = "[0-9a-zA-Z]+$";
iErr = regcomp(&preg, acReg, REG_EXTENDED);
if (iErr) {
printf("compile reg error\n");
exit(1);
}
iErr = regexec(&preg, "12345", 256, subs, 0);
if (REG_NOMATCH == iErr) {
printf("no match\n");
} else {
printf("match\n");
}
}

参考资料:

Comment and share

Unix标准IO库相关函数总结之读写流(二)

读写流的三种方式

一旦一个流被打开,则可以选择三种方式对其进行读写。

  • 每次一个字符的IO,一次读或者写一个字符,如果流是带缓冲的,则标准IO函数处理所有的缓冲。
  • 每次一行的IO。每次读写一行数据,可以使用fgets和fputs函数,但是要说明最大行的长度。
  • 直接IO。通常使用fread和fwrite函数。

一个字符的IO

输入函数

通常使用以下三个函数进行一个字符的读。

1
2
3
4
#include <stdio.h>
int getc(FILE*);
int fgetc(FILE*);
int getchar();

区别与联系

这三个函数都用于一个字符的读取。其区别和联系如下:

  • getchar()相当于getc(stdin)。即每次从标准输入流读入一个字符。实质上getc是宏
  • getc和fgetc的区别是getc可以被实现为宏,而fgetc不能被实现为宏。这就意味着:
    • getc的参数不能是具有副作用的表达式,因为它的值可能被计算多次。
    • fgetc是一个函数,可以获得其地址, 这就允许将fgetc作为参数传递给另一个函数。
    • 调用fgetc时间比getc时间长,因为调用宏的时间更短。

有副作用的表达式是指:表达式的作用本质是用于计算的,原则上只返回一个计算结果,而不会改变表达式中的变量的值。这种不改变表达式中变量值的表达式叫做无副作用的表达式。如:x+y,y-z等。除此之外,若表达式中变量的值被改变则成为有副作用的表达式,如x++,y+=2;

由于在宏中宏可能出现在程序的很多位置,也就是表达式会被计算多次,这个时候若表达式有副作用就会GG。

返回值

这三个函数的返回值都是int类型,这三个函数在返回下一个字符的时候,将其unsigned char类型转换为int。说明无符号的理由是,如果最高位为1,也不会使返回值为负。返回整形的理由是这样既可以返回所有的字符,也可以返回出错或到达文件为的指示值

注意不管是到达文件为还是出错,这个时候三个函数的返回值都一样。为了区分这两种情况,常调用ferror或feof函数。

1
2
3
4
5
6
7
#include <stdio.h>
//用于判断流fp是否遇到读取错误,若读取错误,返回非零值,否则返回0。
int ferror(FILE* fp);
//用于判断流是否遇到文件结尾,若到达文件结尾,返回非零值,否则返回0.
int feof(FILE* fp);
void clearerr(FILE* fp);

在大多数的实现中,为每个流在FILE对象中维护了两个标志:

  • 出错标志
  • 文件结束标志

调用clearerr可以清除这两个标志。

压送字符到流中。

从流中读取字符以后,可以使用ungetc将字符押送回流中,压回的字符又可以从流中读出,读出的顺序与压送的顺序相反。压送的字符不会被写到流中。

1
2
#include <stdio.h>
int ungetc(int c, FILE* fp);

###输出函数
输出函数为以下三个,与输入对应,区别与联系和输入函数相同。

1
2
3
4
#include <stdio.h>
int putc(int c, FILE* fp);
int fputc(int c, FILE* fp);
int putchar(int c);

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE* test = fopen("test.txt", "rw");
char a;
while ((a =getc(test)) != EOF) {
putc(a, stdout);
putc('\n', stdout);
}
ungetc('1', test);
ungetc('2', test);
putc(getc(test), stdout);
putc(getc(test), stdout);
if (ferror(test)) {
printf("this is the read error\n");
}
if (feof(test)) {
printf("this is the eof\n");
}
fclose(test);
}

一行的IO

输入

标准IO中提供了一下两个函数进行一行的读取:

1
2
3
#include <stdio.h>
char* fgets(char* restrict buf, int n, FILE* fp);
char* gets(char* buf);

返回值

若读取成功返回buf,若读取失败或者读取到文件结尾返回NULL.

区别与联系

  • 对于fgets必须指定缓冲区的长度n.此函数一直读到下一个换行符为止,但是不能超过n-1个字符,读入的字符将被送入缓冲区,该缓冲区以null结尾。若改行包含换行符超过了n-1个字符,fgets只返回一个不完整的行。下一次读取的时候将继续从该行继续往下读。
  • gets函数用于从标准输入读取,但是gets不包含缓冲区的长度,所以在读取的时候可能会出现缓冲区溢出的情况。一般最好不要用gets函数.
  • gets函数不将换行符存入缓冲区,而fgets将换行符存入缓冲区

虽然ISO要求提供gets函数,但一般使用fgets不要使用gets函数。

输出

对应的,标准IO提供了以下连个函数进行输出:

1
2
3
#include <stdio.h>
int fputs(const char* restrict buf, FILE* restrict fp);
int puts(const char* buf);

返回值

成功返回非负值,失败返回EOF。

区别

  • fputs将一个以null结尾的字符串写到指定的流,但是null不写出,注意这不是每次输出一行,而取决于缓冲区中的内容。
  • puts将一个以null结尾的字符串写到指定的流,null不写出,但是在最后又添加了一个换行符
  • 一般情况下使用fgets和fputs,不适用gets和puts函数。

实例

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
FILE* fp = fopen("test.txt", "rw");
char buf[5];
while (fgets(buf, 5, fp)) {
printf("%s", buf);
}
return 0;
}

直接IO

直接IO通常用于对二进制文件的读写,除此之外也可以对文本文件进行读写。其中对二进制文件的读写只能用直接IO的方式,因为二进制文件中可能含有null字符,会导致使用行或者字符获取时出现错误。直接IO进行读写的两个函数如下:

1
2
3
4
#include <stdio.h>
size_t fread(void* restrict ptr, size_t size, size_t nobj, FILE* fp);
size_t fwrite(const void* restrict ptr, size_t size, size_t nobj, FILE* fp);

参数说明

  • ptr:为缓冲区指针
  • size:为一个结构体(类型)的大小
  • nobj:结构体的个数,若在fread中说明将要写入nobj个大小为size的结构体(类型),若为fwrite,则为要从流fp中读取nobj个大小为size的数据
  • fp:文件指针

返回值

返回值为实际读取或写入的对象的个数。对于fread,若文件出错或者读到文件结尾处都可以少于nobj,对于fwrite,若返回值小于nobj,则写入出错。

注意

使用这两个函数存在一个问题就是,他们只能用于读写在同一个系统上已写的数据。若是通过网络挂载的文件则不可行。
fread,fwrie可移植,而read,write不可移植。

格式化输出

格式化输出有以下几个函数:

1
2
3
4
5
6
7
#include <stdio.h>
int printf(const char* restrict format, ...);
int fprintf(FILE* restrict fp, const char* restrict format, ...);
int dprintf(int fd, const char* restrict format, ...);
int sprintf(char* restrict buf, const char* restrict format, ...);
int snprintf(char* restrict buf, size_t n, const char* restrict format, ...);

返回值

前三个函数若输出成功,则返回输出字符的个数;若输出出错,则返回负值。

sprintf和snprintf若执行成功,则返回存入buf中的字符串的长度,否则返回负值。

函数说明

  • printf:向标准输出输出字符串。
  • fprintf:向标准文件流输fp出字符串。
  • dprintf:向文件描述符所指向的文件输出字符串。
  • sprintf:向缓冲区buf写入字符串。
  • snprintf:安全的向缓冲区buf写入字符串并指定缓冲区的最大长度.一般用此函数代替sprintf

格式化字符串format的格式

1
2
//其中[]表示可选部分,converter不可选
%[flags][fldwidth][precision][lenmodifier]converter
  • flags : 是该输出的标志,其包含如下几个值:
    • ‘ : 将整数按千位分组字符。
      • : 在字段内左对齐输出。
      • : 总是显示正负号
    • (空格) : 如果第一个字符不是正负号,则在其前面加上一个空格
    • : 指定另一种转换形式,如对于16进制,在前面加0x

    • 0 : 添加前导0进行填充
  • fldwidth : 说明最小字段宽度,若参数字段小于此宽度,多余位置用空格填充。
  • precision : 浮点数精度。前导为.

注意:fldwidth可以用*作为占位符,然后在后面对其进行指定。

1
2
3
4
5
6
7
8
#include <stdio.h>
int main () {
// 10.10
printf("%10.2lf", 10.1);
//与上面写法等效。
printf("%*.*lf", 10, 2, 10.1);
}

Comment and share

Unix标准IO库相关函数总结之打开关闭流(一)

Unix标准IO类型FILE

在Unix相关的文件IO中几乎所有的函数都用到了文件描述符,文件描述符是打开一个文件时返回的一个可用的最小的文件描述标识。相应的在Unix标准IO相关的函数中,几乎每个函数都用到了FILE数据类型。本小结简单介绍一下FILE结构体的内容。

FILE实际上是一个struct的typedef,可以在/usr/include/stdio.h中找到它的定义为:

1
typedef _IO_FILE FILE;

_IO_FILE_的定义在文件/usr/include/libio.h中,我们可以看到它的具体定义为:

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

从上面的struct中可以观察到FILE中是有文件描述符标志的,即为fileno参数。

打开标准IO流

打开标准文件IO流的方法有如下三个:

1
2
3
4
5
FILE* fopen(const char* restrict pathname, const char* restrict type);
FILE* freopen(const char* restrict pathname, const char* restrict type, FILE* restrict fp);
FILE* fdopen(int fd, const char* type);

上述三个函数区别以及使用场景说明:

  • fopen的作用是打开一个指定文件路径的文件流。
  • freopen的作用是在一个指定的留上打开一个指定文件。如果当前流已经打开,则先关闭该流;若该流被重定向,则清除重定向。该函数常用于将一个指定的文件打开为一个默认的流,如若想使用printf函数将打印出来的内容输出到一个指定文件中,此时可以讲stdout重定向到指定的文件路径并指定打开模式。
  • fdopen的作用是将一个已经打开的文件描述符(该文件描述符可能是open,dup,socket等获取的)绑定到一个标准IO流上。此函数常用于由管道和网络通信通道函数返回的文件描述符,因为这些无法显式的指定文件。

参数说明:

  • pathname: 文件路径,相对或绝对
  • type: 打开模式(r,w,a,r+,w+,a+以及所有后面加b)

打开模式说明:

  • r : 读模式
  • w : 写模式,在写之前将原有文件内容全部清楚
  • a : 追加模式,offset为文件结尾
  • r+ : 读写模式(不删除文件原有内容,offset初始为文件开头)。如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc456”。
  • w+ : 读写模式(删除文件原有内容,offset为文件开头).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”abc”。
  • a+ : 读写模式(offset为文件结尾).如原有文件内容为”123456”,若以此模式打开文件并写入”abc”,此时写入之后的结果是”123456abc”。

上述所有的模式后面都加上b表示对二进制文件的操作(rb,wb,ab,rb+,wb+,ab+)。

注意:对于fdopen函数由于文件已经由文件描述符打开,此时w模式时将不清除文件内容,追加模式不常见文件。

当以读写模式打开文件时候,将有一些限制。如果中间没有fflush,fseek,fsetpos以及rewind函数,标注输出之后不能直接进行输入;如果没有fseek,fsetpos或者rewind,或者一个输入没有到达文件尾,则输入操作之后不能跟输出操作。

关闭标准文件流

一般情况下在关闭文件流之前通常先使用fflush刷新缓冲区防止数据丢失,关闭标准文件流使用的函数为:

1
2
#include <stdio.h>
int fclose(FILE* fp);

Comment and share

TCP状态转换图

TCP涉及连接建立和连接终止的操作可以用状态转换图来说明。

TCP为一个连接定义了11中状态,并且规定了如何基于当前状态以及该状态下接收的分节(TCP报文段)从一个状态转换为另一个状态。其状态转换图如下所示:

具体转化过程和转换条件如上图所示。

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China