调试厉器addr2line

addr2line: 将地址转换为文件名和行号的命令行工具

在C/C++程序的调试过程中,我们通常会使用调试器(如GDB)来定位崩溃或错误的位置。但有时候,我们可能只能获得程序崩溃时的地址,而没有调试器的支持。这时候,addr2line就可以帮助我们将地址转换为对应的源代码文件和行号。

addr2line的原理

在程序编译时,编译器通常会将源代码的调试信息嵌入到可执行文件中。这些调试信息包括每个函数的起始地址、每个源代码文件的文件名和行号等。当程序崩溃或遇到错误时,我们可以根据崩溃位置的地址,通过addr2line工具将其转换为对应的源代码文件和行号。

addr2line的工作原理如下:

  1. 读取可执行文件中的调试信息。

  2. 根据地址查找对应的函数。

  3. 根据函数的调试信息查找对应的源代码文件。

  4. 根据源代码文件的调试信息查找对应的行号。

  5. 输出文件名和行号。

安装

addr2line是GNU Binutils工具集中的一部分,可以在Linux和macOS系统中进行安装。在Ubuntu系统中,可以使用以下命令进行安装:

$ sudo apt-get install binutils

在macOS系统中,可以使用Homebrew包管理器进行安装:

$ brew install binutils

使用

addr2line将地址转换为文件名和行号。给定可执行文件中的地址或可重定位对象部分中的偏移量,它会使用调试信息来确定与之相关的文件名和行数。

通过addr2line的描述可知,在使用addr2line将地址转换为函数、文件名或行号时,其有两种使用方法:即对于可执行文件,addr2line后直接跟十六进制的地址值;对于可重定位对象文件,addr2line后直接跟十六进制的地址偏移量。从而实现正确输出需要的信息,否则会导致地址无法解析,输出??:0。

注:如何区分可执行文件与可重定位对象

#输出executable,可知文件是可执行文件
[root@localhost ~]# file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6bae72d43422a7a048ecf8bce55723aff816820b            , with debug_info, not stripped
#输出relocatable,可知文件是可重定位对象
[root@localhost ~]# file helloworld.ko
helloworld.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=2f5b2cbb443e05489dcf0272dbb0b72875172411, with debug_info, not stripped

基本用法

基本用法:addr2line [选项] [地址]
选项描述
-j–section= 读取相对于段的偏移而非地址
-p–pretty-print 让输出对人类更可读
-s–basenames 去除目录名
-f–functions 显示函数名
-C–demangle[=style] 解码函数名
-h–help 显示本帮助
-a–addresses 显示地址
-b–target= 设置二进位文件格式
-e–exe= 设置输入文件名称(默认为 a.out)
-i–inlines 解开内联函数

程序奔溃场景

1、用户态普通程序
使用方法:addr2line -e 进程名 IP指令地址 -f

用户态程序有时可能因为各种原因导致崩溃,发生段错误,比如空指针等。如果没有靠谱的工具,我们就只能靠猜哪里的代码可能存在问题,这里通过Linux自带的addr2line工具调试程序,能够快速直接帮我们准确定位到文件、异常函数名以及行号。
example.c源文件:

#include <stdio.h>
int main()
{
       int *p = NULL;
       *p = 0;

       return 0;
}

使用gcc进行编译,如下:

[root@localhost 68]# gcc example.c -o example -g
[root@localhost 68]# ls
example  example.c
[root@localhost 68]# ./example
Segmentation fault (core dumped)

dmesg查看报错信息,如下:

[root@localhost ~]# dmesg
[134563.793925] example[53791]: segfault at 0 ip 0000000000400546 sp 00007fff7956af70 error 6 in example[400000+1000]
[134563.793946] Code: 01 5d c3 90 c3 66 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa eb 8a 55 48 89 e5 48 c7 45 f8 00 00 00 00 48 8b 45 f8 <c7> 00 00 00 00 00 b8 00 00 00 00 5d c3 66 2e 0f 1f 84 00 00 00 00
[root@localhost 68]# addr2line -e example 0000000000400546 -f
main
/tmp/68/example.c:5

该例子说明addr2line能够直观程序发生段错误的函数以及文件和行号。

:如果编译程序时没有加上-g参数(即程序不含调试信息),就只能显示出函数名,显示不出具体所在文件的位置,如下:

[root@localhost 68]# addr2line -e example 0x0000000000400546 -f
main
??:?

提示:如何区分程序是否还有调试信息,可通过file命令,如果输出结果中显示with debug_info则表示程序含有调试信息,否则表示不含有调试信息。

#不含有调试信息
[root@localhost 68]# file example
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=135f8128f721d829b9318da6105cc47223b14de9, not stripped
#含有调试信息
[root@localhost 68]# file example
example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=c94635b3ef107e7ecc19f4c1a16e16f031d9ca51, with debug_info, not stripped
2、动态库程序

在动态库中发生段错误,也可以使用addr2line进行定位。

使用方法:addr2line -e 动态库名 IP指令地址-基地址 -f

test.c源文件:

#include "foo.h"

int main(void)
{
    foo();
    return 0;
}

foo.h:

#ifndef __FOO_LIB_H__
#define __FOO_LIB_H__

int foo(void);

#endif

foo.c:

#include "foo.h"

int foo()
{
    int *p = 0;
    *p = 0;
    return 0;
}

先编译动态库, 再编译主程序, 让它链接动态库, 最后运行之:

[root@localhost 69]# gcc -O3 -g -o libfoo.so -shared -fPIC foo.c
[root@localhost 69]# gcc -O3 -g -o test test.c -L. -lfoo 
[root@localhost 69]# export LD_LIBRARY_PATH=. 
[root@localhost 69]# ./test
Segmentation fault (core dumped)

查看dmesg日志,如下:

[root@localhost ~]# dmesg
[70567.416655] test[27722]: segfault at 0 ip 00007ffa1f588580 sp 00007fffa964e698 error 6 in libfoo.so[7ffa1f588000+1000]
[70567.427374] Code: ff e8 64 ff ff ff c6 05 bd 0a 20 00 01 5d c3 0f 1f 00 c3 0f 1f 80 00 00 00 00 f3 0f 1e fa e9 77 ff ff ff 0f 1f 80 00 00 00 00 <c7> 04 25 00 00 00 00 00 00 00 00 0f 0b 00 00 00 f3 0f 1e fa 48 83

根据日志可知,段错误发生的位置是在test进程调用的libfoo.so库里,我们先使用ldd找到动态库的位置,如下:

[root@localhost 69]# ldd test
        linux-vdso.so.1 (0x00007ffd15b24000)
        libfoo.so => ./libfoo.so (0x00007f8c01879000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f8c014b4000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8c01a7b000)

00007ffa1f588580为程序崩溃点ip指令地址,使用dmesg消息中ip指令地址减去动态库基址值(00007ffa1f588580 -7ffa1f588000=580), 差值0x580为错误点在动态库的地址,调用addr2line, 注意-e参数后文件名改为动态库名:

[root@localhost 69]# addr2line -e libfoo.so 580 -f -p
foo at /tmp/69/foo.c:6

动态库基址值(模块载入地址)即dmesg消息libfoo.so[7ffa1f588000+1000]语句中的7ffa1f588000值。

3、内核模块程序
使用方法:addr2line -e xxx.ko 地址偏移量 -f

当内核模块程序异常时,可能会导致机器直接死机重启,这种情况下定位bug可能就比较麻烦,dmesg日志在机器重新启动后,新的日志会覆盖掉原来的报错日志,从而对定位内核异常问题造成麻烦。常见

提示:Linux内核错误

  1. panic 当内核遇到严重错误的时候,内核panic,立马崩溃。死机。
  2. Oops Oops是内核遇到错误时发出的提示“声音”,Oops有时候会触发panic,有时候不会,而是直接杀死当前进程,系统可以继续运行。

比如说内核态下的段错误,当内核设置了panic_on_oops=1的时候,Oops会触发panic。【panic_on_oops的值在内核编译的时候配置,可以在/proc/sys/kernel/panic_on_oops查看值,同时可以使用sysctl修改】

当panic_on_oops=0的时候,如果错误发生在中断上下文,Oops也会触发panic。如果错误只是发生在进程上下文,这个时候只需要kill当前进程。【中断上下文包括以下情况:硬中断、软中断、NMI】。Oops的时候内核还可以运行,只是可能不稳定,这个时候,内核会调用printk打印输出内核栈的信息和寄存器的信息。

本人所用主机即属于一旦发生Oops,就会触发panic,因此总是无法查看Oops时的dmesg日志,经查阅资料,发现是内核参数panic_on_oops的原因导致的,因为该参数被设置为1,所以Oops会触发panic,从而导致机器总是死机重启,无法查看Oops时的dmesg日志。下面提供两种方法修改Oops内核参数,使其不会在Oops的时候触发panic导致死机重启。

方法一:修改 /proc下内核参数文件内容,临时生效,重启后失效。

echo 0 > /proc/sys/kernel/panic_on_oops

方法二:修改/etc/sysctl.conf 文件的内核参数来永久更改。

[root@localhost ~]# vi /etc/sysctl.conf
[root@localhost ~]# cat /etc/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
kernel.panic_on_oops=0
[root@localhost ~]# cat /proc/sys/kernel/panic_on_oops
1 
[root@localhost ~]# sysctl -p
kernel.panic_on_oops = 0
[root@localhost ~]#
[root@localhost ~]# cat /proc/sys/kernel/panic_on_oops
0

设置好Oops内核参数以后,让我们来构造生成Oops的程序,源代码如下:

oops.c源文件

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static noinline void do_oops(void)
{
    *(int *)0 = 0;
}

static int my_oops_init(void)
{
    pr_info("my_oops_init\n");
    do_oops();

    return 0;
}

static void my_oops_exit(void)
{
    pr_info("my_oops exit\n");
}

module_init(my_oops_init);
module_exit(my_oops_exit);
MODULE_DESCRIPTION ("Kernel OOPS Testing");
MODULE_LICENSE("GPL");

编译内核模块的Makefile文件

ifneq ($(KERNELRELEASE),)
EXTRA_CFLAGS = -Wall -g
obj-m := oops.o
else
PWD  := $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
        rm -rf .*.cmd *.o *.order *.symvers *.mod.c *.ko .tmp_versions
endif

编译以及安装oops.ko模块,出现killed,由于错误未发生在中断上下文,未导致Kernel panic死机重启。

[root@localhost oops]# make
make -C /lib/modules/4.18.0-394.el8.x86_64/build M=/tmp/oops modules
make[1]: Entering directory '/usr/src/kernels/4.18.0-394.el8.x86_64'
  CC [M]  /tmp/oops/oops.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /tmp/oops/oops.mod.o
  LD [M]  /tmp/oops/oops.ko
make[1]: Leaving directory '/usr/src/kernels/4.18.0-394.el8.x86_64'
[root@localhost oops]#
[root@localhost oops]# ls
dump.log  Makefile  modules.order  Module.symvers  oops.c  oops.ko  oops.mod.c  oops.mod.o  oops.o
[root@localhost oops]# insmod ./oops.ko
Killed 
[root@localhost oops]# lsmod | grep oops
oops                   16384  1

加载模块将生成以下Kernel Oops消息,dmesg查看信息如下:

[root@localhost ~]# dmesg
[ 1039.918606] my_oops_init
[ 1039.918616] BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
[ 1039.926442] PGD 0 P4D 0
[ 1039.928979] Oops: 0002 [#1] SMP NOPTI
[ 1039.932637] CPU: 34 PID: 3843 Comm: insmod Kdump: loaded Tainted: G           OE    --------- -  - 4.18.0-394.el8.x86_64 #1
[ 1039.943756] Hardware name: New H3C Technologies Co., Ltd. H3C UniServer R4950 G5/RS45M2C9SB, BIOS 5.37 09/30/2021
[ 1039.954000] RIP: 0010:do_oops+0x5/0x11 [oops]
[ 1039.958364] Code: Unable to access opcode bytes at RIP 0xffffffffc02e6fdb.
[ 1039.965231] RSP: 0018:ffffb9d40a8c7cb0 EFLAGS: 00010246
[ 1039.970449] RAX: 000000000000000c RBX: 0000000000000000 RCX: 0000000000000000
[ 1039.977573] RDX: 0000000000000000 RSI: ffff98942ee96758 RDI: ffff98942ee96758
[ 1039.984697] RBP: ffffffffc02e7011 R08: 0000000000000000 R09: c0000000ffff7fff
[ 1039.991822] R10: 0000000000000001 R11: ffffb9d40a8c7ad8 R12: ffffffffc02e9000
[ 1039.998944] R13: ffffffffc02e9018 R14: ffffffffc02e91d0 R15: 0000000000000000
[ 1040.006069] FS:  00007f1b8d93b740(0000) GS:ffff98942ee80000(0000) knlGS:0000000000000000
[ 1040.014145] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1040.019884] CR2: ffffffffc02e6fdb CR3: 0000000145c02000 CR4: 0000000000350ee0
[ 1040.027008] Call Trace:
[ 1040.029454]  my_oops_init+0x16/0x19 [oops]
[ 1040.033550]  do_one_initcall+0x46/0x1d0
[ 1040.037390]  ? do_init_module+0x22/0x220
[ 1040.041318]  ? kmem_cache_alloc_trace+0x142/0x280
[ 1040.046023]  do_init_module+0x5a/0x220
[ 1040.049777]  load_module+0x14ba/0x17f0
[ 1040.053530]  ? __do_sys_finit_module+0xb1/0x110
[ 1040.058059]  __do_sys_finit_module+0xb1/0x110
[ 1040.062411]  do_syscall_64+0x5b/0x1a0
[ 1040.066077]  entry_SYSCALL_64_after_hwframe+0x65/0xca
[ 1040.071130] RIP: 0033:0x7f1b8c8509bd
[ 1040.074701] Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d 9b 54 38 00 f7 d8 64 89 01 48
[ 1040.093446] RSP: 002b:00007ffc4df0a968 EFLAGS: 00000246 ORIG_RAX: 0000000000000139
[ 1040.101004] RAX: ffffffffffffffda RBX: 00005653fb1997d0 RCX: 00007f1b8c8509bd
[ 1040.108126] RDX: 0000000000000000 RSI: 00005653f980c8b6 RDI: 0000000000000003
[ 1040.115251] RBP: 00005653f980c8b6 R08: 0000000000000000 R09: 00007f1b8cbd9760
[ 1040.122375] R10: 0000000000000003 R11: 0000000000000246 R12: 0000000000000000
[ 1040.129498] R13: 00005653fb1997b0 R14: 0000000000000000 R15: 0000000000000000
[ 1040.136623] Modules linked in: oops(OE+) binfmt_misc xt_CHECKSUM ipt_MASQUERADE xt_conntrack ipt_REJECT nf_reject_ipv4 nft_compat nft_counter nft_chain_nat nf_nat nf_conntrack nf_defrag_ipv6 nf_defrag_ipv4 nf_tables nfnetlink rpcsec_gss_krb5 auth_rpcgss nfsv4 dns_resolver nfs lockd grace fscache bridge stp llc intel_rapl_msr intel_rapl_common amd64_edac_mod edac_mce_amd amd_energy kvm_amd kvm irqbypass ipmi_ssif pcspkr crct10dif_pclmul crc32_pclmul ghash_clmulni_intel rapl joydev ccp sp5100_tco i2c_piix4 k10temp ptdma acpi_ipmi ipmi_si sunrpc vfat fat xfs libcrc32c sd_mod t10_pi sg crc32c_intel ast drm_vram_helper drm_kms_helper syscopyarea sysfillrect sysimgblt fb_sys_fops drm_ttm_helper ttm ahci drm libahci nfp(OE) igb libata dca i2c_algo_bit dm_mirror dm_region_hash dm_log dm_mod ipmi_devintf ipmi_msghandler
[ 1040.208357] CR2: 0000000000000000
[ 1040.211668] ---[ end trace b69c1e8998070273 ]---
[ 1040.230185] RIP: 0010:do_oops+0x5/0x11 [oops]
[ 1040.234540] Code: Unable to access opcode bytes at RIP 0xffffffffc02e6fdb.
[ 1040.241409] RSP: 0018:ffffb9d40a8c7cb0 EFLAGS: 00010246
[ 1040.246626] RAX: 000000000000000c RBX: 0000000000000000 RCX: 0000000000000000
[ 1040.253750] RDX: 0000000000000000 RSI: ffff98942ee96758 RDI: ffff98942ee96758
[ 1040.260876] RBP: ffffffffc02e7011 R08: 0000000000000000 R09: c0000000ffff7fff
[ 1040.267998] R10: 0000000000000001 R11: ffffb9d40a8c7ad8 R12: ffffffffc02e9000
[ 1040.275124] R13: ffffffffc02e9018 R14: ffffffffc02e91d0 R15: 0000000000000000
[ 1040.282247] FS:  00007f1b8d93b740(0000) GS:ffff98942ee80000(0000) knlGS:0000000000000000
[ 1040.290323] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1040.296061] CR2: ffffffffc02e6fdb CR3: 0000000145c02000 CR4: 0000000000350ee0

消息解析:

[ 1039.928979] Oops: 0002 [#1] SMP NOPTI
[ 1039.932637] CPU: 34 PID: 3843 Comm: insmod Kdump: loaded Tainted: G           OE    --------- -  - 4.18.0-394.el8.x86_64 #1 

Oops: 0002 – 错误码

Oops: [#1] – Oops发生的次数

CPU: 34 – 表示Oops是发生在CPU34上

关键信息如下,这里提示在操作函数do_oops的时候出现异常,地址偏移量0x5:
[ 1039.954000] RIP: 0010:do_oops+0x5/0x11 [oops]

为什么这条信息关键,因为其含有指令指针RIP;指令指针IP/EIP/RIP的基本功能是指向要执行的下一条地址。在8080 8位微处理器上的寄存器名称是PC(program counter,程序计数器),从8086起,被称为IP(instruction pointer,指令指针)。主要区别在与PC指向正在执行的指令,而IP指向下一条指令。在64位模式下,指令指针是RIP寄存器。这个寄存器保存着下一条要执行的指令的64位地址偏移量。64位模式支持一种新的寻址模式,被称为RIP相对寻址。使用这个模式,有效地址的计算方式变为RIP(指向下一条指令)加上位移量。

由此可以看出内核执行到do_oops+0x5/0x11这个地址的时候出现异常,我们只需要找到这个地址对应的代码即可。

打印格式:do_oops+0x5/0x11 [oops] 即:symbol+offset/size [module] symbol: 符号 offset:地址偏移量 size:函数的长度 module: 所属内核模块

do_oops指示了是在do_oops函数中出现的异常, 0x5表示出错的地址偏移量, 0x11表示do_oops函数的大小。使用file查看内核模块文件类型:

[root@localhost oops]# file oops.ko
oops.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=4b5b58c4aaf63d65d338be17399bfd3c480504c3, with debug_info, not stripped

上述结果显示该内核模块文件为可重定位对象文件,因此使用addr2line直接跟十六进制的地址偏移量定位文件名、行号和行数,如下:

[root@localhost oops]# addr2line -e oops.ko 0x5 -f -p
do_oops at /tmp/oops/oops.c:7

可以看到异常代码在oops.c文件第7行,根据源代码,可知该行代码访问非法内存地址

注意事项

在使用addr2line时,需要注意以下几点:

  1. 可执行文件必须包含调试信息才能使用addr2line进行转换。如果编译时没有开启调试信息选项(如-g),则可能无法转换。

  2. 转换结果可能不一定准确。因为在优化编译时,编译器会对代码进行一些优化,可能会导致源代码和可执行代码之间存在一些差异。这时候,转换结果可能会不准确。

  3. 在转换地址时,要注意地址的字节序。如果程序是在大端模式或小端模式下编译的,而addr2line使用的字节序不同,则转换结果也会不正确。

总结

Addr2line 工具(它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。这在应用程序和内核程序执行过程中出现崩溃时,可用于快速定位出出错的位置,进而找出代码的bug。一般适用于 debug 版本或带有 symbol 信息的库。

参考

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
摘自:http://mbstudio.spaces.live.com/blog/cns!C898C3C40396DC11!955.entry 2007/1/30 oSIP协议栈(及eXoSIP,Ortp等)使用入门(原创更新中) (CopyLeft by Meineson | www.mbstudio.cn,原创文章,欢迎转载,但请保留出处说明!) 本文档最新版本及文中提到的相关源码及VC6工程文件请在本站找,嘿嘿~~ (首页的SkyDriver公开文件夹中,可能需要用代理才能正常访问该空间——空间绝对稳定,不会丢失文件!) (最近工作重心不在SIP开发,SO本文档也没有机会更新,有技术问题也请尽量咨询他人,本人不一定能及时回复。)   一直没空仔细研究下oSIP,最近看到其版本已经到了3.x版本,看到网上的许多帮助说明手册都过于陈旧,且很多文档内容有点误人子弟的嫌疑~~   Linux下oSIP的编译使用应该是很简单的,其Install说明文档里也介绍的比较清楚,本文主要就oSIP在Windows平台下VC6.0开发环境下的使用作出描述。   虽然oSIP的开发人员也说明了,oSIP只使用了标准C开发库,但许多人在Windows下使用oSIP时,第一步就被卡住了,得不到oSIP的LIB库和DLL库,也就没有办法将oSIP使用到自己的程序中去,所以第一步,我们将学习如何得到oSIP的静态和动态链接库,以便我们自己的程序能够使用它们来成功编译和执行我们的程序。 第一阶段: ------------------------------------------------------   先创建新工程,网上许多文档都介绍创建一个Win32动态链接库工程,我们这里也一样,创建一个空白的工程保存。   同样,将oSIP2版本3.0.1 src目录下的Osipparser2目录下的所有文件都拷到我们刚创建的工程的根目录下,在VC6上操作: Project-Add To Project-Files   将所有的源程序和头文件都加入到工程内,保存工程。   这时,我们可以尝试编译一下工程,你会得到许多错误提示信息,其内容无非是找不到osipparser2/xxxxx.h头文件之类。   处理:在Linux下,我们一般是将头文件,lib库都拷到/usr/inclue;/usr/lib之类的目录下,c源程序里直接写#include 时,能直接去找到它们,在VC里,同样的,最简单的方法就是将oSIP2源码包中的Include目录下的 osipparser2目录直接拷到我们的Windows下默认包含目录即可,这个目录在VC6的Tool-Options-Directories里设置,(当然,如果你知道这一步,也可以不用拷贝文件,直接在这里把oSIP源码包所在目录加进来就可以了),默认如果装在C盘,目录则为 C:\Program Files\Microsoft Visual Studio\VC98\Include。   这时,我们再次编译我们的工程,顺利编译,生成osipparser2.dll,这时,网上很多文档里可能直接就说,这一步也会生成libs目录,里面里osipparser2.lib文件,但我们这里没有生成:)   最简单的方法,不用深究,直接再创建一个工程,同上述创建动态链接库方法,创建一个Win32静态链接库工程,直接编译,即可得到osipparser2.lib。 ------------------------------------------------------   上面,我们得到了Osip的解析器开发库,下面再编译完整的Osip协议栈开发库,同样照上述方法,分别创建动态链接库工程和静态链接库工程,只是要拷的文件换成src下的osip目录下文件和include下的osip目录,得到osip2.dll和osip2.lib。   在编译osip2.dll这一步可能会再次得到错误,内容含义是找不到链接库,所以,我们要把前面编译得到的osipparser2.lib也拷到osip工程目录下,并在VC6中操作:   Project-Setting-Link中的Object/Library Modules: kernel32.lib user32.lib ... xxx.lib之类的内容最后增加: osipparser2.lib   保存工程后再次编译,即可成功编译osip2.dll。 ------------------------------------------------------   至此,我们得到了完整的oSIP开发库,使用时,只需在我们的程序里包含oSIP的头文件,工程的链接参数里增加osipparser2.lib和osip2.lib即可。 ------------------------------------------------------   下面我们验证一下我们得到的开发库,并大概了解一下OSIP的语法规范。   在VC里创建win32控制台程序工程,将libosip源码包的SRC目录下的Test目录内的C源程序随便拷一个到工程时,直接编译(工程设置里照前文方法在link选项里增加osip2.lib,osipparser2.lib引用我们之前成功编译得到的静态库文件)就可以运行(带参数运行,参数一般为一个文本文件,同样从Test目录的res目录里拷一个与源文件同名的纯文本文件到工程目录下即可)。   该目录下的若干文件基本上是测试了Osip的一些基本功能函数,例如URI解析之类,可以大概了解一下oSIP的语法规范和调用方法,同时也能校验一下之前编译的OSIP开发库能否正常使用,成功完成本项工作后,可以进入下一步具体的oSIP的使用学习了。 ------------------------------------------------------   由于oSIP是比较底层的SIP协议栈实现,新手较难上手,而官方的示例大都是一些伪代码,需要有实际的例子程序参考学习,而最好的例子就是同样官方发布的oSIP的扩展开发库exosip2,使用exoSIP可以很方便地快速创建一个完整的SIP程序(只针对性地适用于SIP终端开发用,所以我们这里只是用它快速开发一个SIP终端,用来更方便地学习oSIP,要想真正掌握SIP的开发,需要掌握oSIP并熟读RFC文档才行,exoSIP不是我们的最终学习目的),通过成功编译运行一个自己动手开发出的程序,再由浅入深应该是初学都最好的学习方法通过对使用exosip开发库的使用创建自己的SIP程序,熟悉后再一个函数一个函数地深入学习exosip提供的接口函数,就可以深入理解osip 了,达到间接学习oSIP的目的,同时也能从eXoSIP中学习到正确使用oSIP的良好的编程风格和语法格式。   而要成功编译ExoSIP,似乎许多人被难住了,直接在XP-sp2上,用VC6,虽然你使用了eXoSIP推荐的winsock2.h,但是会得到一个 sockaddr_storage结构不能识别的错误,因为vc6自带的开发库太古董了,需要升级系统的Platform SDK,下载地址如下: http://www.microsoft.com/msdownl ... PSP2FULLInstall.htm(VC6的支持已经停止,这是VC6能使用的最新SDK)   成功安装后编译前需加OSIP_MT宏,以启用线程库,否则在程序中使用eXoSIP库时会出错,而编译时也会得到许多函数未定义的Warning提示,编译得到exosip2.lib供我们使用,当然,在此之前需要成功编译了osip2和osipparser2,而在之后的实际使用时,发现oSIP也需要增加OSIP_MT宏,否则OSIP_MT调用oSIP的线程库时会出错,所以我们需要重新编译oSIP了:),因为eXosip是基于oSIP的(同上方式创建静态和动态链接库工程,并需在Link中手工添加oSIP和oSIPparser的lib库)。 ------------------------------------------------------   创建新工程,可以是任意工程,我们从最简单的Win32控制台程序开始,为了成功使用oSIP,我们需要引用相关库,调用相关头文件,经过多次试验,发现需要引用如下的库: exosip2.lib osip2.lib osipparser2.lib WSock32.Lib IPHlpApi.Lib WS2_32.Lib Dnsapi.lib   其中,除了我们上面编译得到的三个oSIP库外,其它库都是系统库,其中有一些是新安装的Platform SDK所新提供的。   至此,我们有了一个简单的开发环境了,可以充分利用网上大量的以oSIP为基础的代码片段和官方说明文档开始具体函数功能的测试和使用了:) ------------------------------------------------------   我们先进行一个简单的纯SIP信令(不带语音连接建立)的UAC的SIP终端的程序开发的试验(即一个只能作为主叫不能作为被叫的的SIP软电话模型),我们创建一个MFC应用程序,对话框模式,照上面的说明,设置工程包含我们上面得到的oSIP的相关开发库及SDK的一些开发库,并且由于默认LIBC的冲突,需要排除MSVCRT[D]开发库(其中D代表Debug模式下,没有D表示Release模式下),直接使用eXosip的几个主要函数就可以创建一个基本的SIP软电话模型。   其主要流程为:   初始化eXosip库-启动事件监听线程-向SIP Proxy注册-向某SIP终端(电话号码)发起呼叫-建立连接-结束连接   初始化代码: int ret = 0; ret = eXosip_init (); eXosip_set_user_agent("##YouToo0.1"); if(0 != ret) { AfxMessageBox("Couldn't initialize eXosip!\n"); return false; } ret = eXosip_listen_addr (IPPROTO_UDP, NULL, 0, AF_INET, 0); if(0 != ret) { eXosip_quit (); AfxMessageBox("Couldn't initialize transport layer!\n"); return false; }   启动事件监听线程: AfxBeginThread(sip_uac,(void *)this);   向SIP Proxy注册: eXosip_clear_authentication_info(); eXosip_add_authentication_info(uname, uname, upwd, "md5", NULL); real_send_register(30);  /* 自定义函数代码请见源码 */   发起呼叫(构建假的SDP描述,实际软电话使用它构建RTP媒体连接): osip_message_t *invite = NULL; /* 呼叫发起消息体 */ int i = eXosip_call_build_initial_invite (&invite, dest_call, source_call, NULL, "## YouToo test demo!"); if (i != 0) { AfxMessageBox("Intial INVITE failed!\n"); } char localip[128]; eXosip_guess_localip (AF_INET, localip, 128); snprintf (tmp, 4096, "v=0\r\n" "o=josua 0 0 IN IP4 %s\r\n" "s=conversation\r\n" "c=IN IP4 %s\r\n" "t=0 0\r\n" "m=audio %s RTP/AVP 0 8 101\r\n" "a=rtpmap:0 PCMU/8000\r\n" "a=rtpmap:8 PCMA/8000\r\n" "a=rtpmap:101 telephone-event/8000\r\n" "a=fmtp:101 0-11\r\n", localip, localip, "9900"); osip_message_set_body (invite, tmp, strlen(tmp)); osip_message_set_content_type (invite, "application/sdp"); eXosip_lock (); i = eXosip_call_send_initial_invite (invite); eXosip_unlock ();   挂断或取消通话: int ret; ret = eXosip_call_terminate(call_id, dialog_id); if(0 != ret) { AfxMessageBox("hangup/terminate Failed!"); }   可以看到非常简单,再借助于oRTP和Mediastreamer开发库,来快速为我们的SIP软电话增加RTP和与系统语音API接口交互及语音编码功能,即可以快速开发出一个可用的SIP软电话,关于oRTP和Mediastreamer的相关介绍不是本文重点,将在有空的时候考虑增加相应使用教程,文章前提到的地方可以下载基本可用的完整SIP软电话的VC源码工程文件供参考使用,完全CopyLeft,欢迎转载,但请在转载时注明作者信息,谢谢! 第二阶段: ---------------------------------------------------   得到了一个SIP软电话模型后,我们可以根据软电话的实际运行表现(结合用Ethereal抓包分析)来进行代码的分析,以达到利用eXoSIP来辅助我们学习oSIP的最终目的(如要快速开发一个可用的SIP软电话,请至前面提到的论坛去下载使用oRTP和Mediastreamer快速搭建的一个基本完整可用的SIP软电话##YouToo 0.1版本的VC源码工程文件作参考)。   现在从eXosip的初始化函数开始入手,来分析oSIP的使用,这是第二阶段,第三阶段就是深入学习oSIP的源码了,但大多数情况下应该没有必要了,因为在第二阶段就有部分涉及到第三阶段的工作了,而且oSIP的源码也就大多是一些SIP数据的语法解析和状态机的实现,能深入理解了SIP协议后,这些只是一种实现方式,没必要完全去接受,而是可以用自己的方式和风格来实现一套,比如,更轻量化更有适用目的性的方式,oSIP则只起参考作用了。   eXosip_init()是eXosip的初始化函数,我们来看看它的内部实现:   首行是定义的 osip_t *osip,这在oSIP的官方手册里我们看到,所有使用oSIP的程序都要在最开始处声明一个osip_t的指针,并使用 osip_init(&osip)来初始化这个指针,销毁这个资源使用osip_release(osip)即可。   我们可以在代码中看到很多OSIP_TRACE,这是调试输出宏调用了函数osip_trace,可以用ENABLE_TRACE宏来打开调试以方便我们开发调试。   其它就是很多的eXosip_t的全局变量eXosip的一些初始化操作,包括最上面的memset (&eXosip, 0, sizeof (eXosip))完全清空和下面的类似eXosip.user_agent = osip_strdup ("eXosip/" EXOSIP_VERSION)的exosip变量的一些初始值设置,其中有一个eXosip.j_stop_ua = 0应该是一个状态机开关,后面可以看到很多代码检测这个变量来决定是否继续流程处理,默认置成了0表示现在exosip的处理流程是就绪的,即ua是 not stop的。      osip_set_application_context (osip, &eXosip)是比较有意思的,它让下面的eXosip_set_callbacks (osip)给osip设置大量的回调函数时,能让osip能访问到eXosip这个全局变量中设置的大量程序运行时交互的信息,相当于我们在VC下开启一个线程时,给线程传入的一个void指针指向我们的MFC应用程序的当前dialog对象实例,可以用void *osip_get_application_context (osip_t * osip)这个函数来取出指针来使用,不过好象exosip中并没有用到它,可能是留给个人自已扩展的吧:)      还能看到初始化代码前面有一段WIN32平台下的SOCK的初始化代码,可以知道eXosip是用的原生的winsock api函数,也就是我们可能以前学过的用VC和WINAPI写sock程序时(不是MFC),用到的那段SOCK初始代码,还有一段有意思的代码,就是 jpipe()函数,它们返回的是一个管道,一个有2个整型数值的数组(一个进一个出),查看其代码发现,非WIN32平台是直接使用的pipe系统函数,而WIN32下则是用一对TCP的本地SOCK连接来模拟的管道,一个SOCK写一个SOCK读,这段代码是比较有参考价值的:) j = 50; while (aport++ && j-- > 0) {   raddr.sin_port = htons ((short) aport);   if (bind (s, (struct sockaddr *) &raddr, sizeof (raddr)) transactionid)); }   即,只是打印一下调试,并没有完整实现什么功能,我们学习时,完全可以用相同的方法,定义一大堆回调函数,并不忙想怎么完全实现,先都是只打印一下调试信息,看具体的应用逻辑根据抓包测试分析和看调试看程序走到了哪一步,调用了哪一个回调,来明白具体回调函数要实现什么用途,再来实现代码就方便多了,当然,如果看透了RFC文档,应该从字面就能知道各个回调函数的用途了,这是后话,不是谁都能快速完全看懂RFC的,所以我们要参考eXosip:)      我们对其中的重要的回调函数进行逐个的分析:   ---------------------------   osip_set_cb_send_message (osip, &cb_snd_message) SIP消息发送回调函数   这个函数可能是最重要的回调函数之一,消息发送,包括请求消息和回应消息,一般情况下,状态机的状态就是由它控制的,发起一个消息初始化一个状态机,回应一个消息对状态机修改,终结消息发送结束状态机……   看cb_snd_message的函数实现,要以发现,其主要代码是对参数中的要发送的消息osip_message_t * sip进行分析,找出消息要发送的真实char *host,int port的值(这些参数可以省略,但要发送消息肯定需要host和port,所以要从sip中解析),最后根据sip中解析出的传输方式是TCP还是 UDP选择最终进行消息发送处理的函数cb_udp_snd_message,cb_tcp_snd_message处理(它们的参数一致,即本函数只是补全一些省略的参数并对消息进行合法性检查)。   **毕竟eXosip是一个通用的开发库,它考虑了要支持TCP,UDP,TCPs,IPV4,IPV6,WIN32,*nix,WINCE等等多样化的复杂环境,所以,我们可以略过我们暂时不需要的部分,比如,IPV6相关的代码实现等。      由于我们大多数情况下SIP是用的UDP,所以先来看一下cb_udp_snd_message的实现,它从全局变量exosip中获取可用的 sock,并尽最大能力解析出host和port(??难道前面的函数还不够解析彻底??如最终仍无port信息则默认设置为5060),使用 osip_message_to_str (sip, &message, &length)函数将要发送的格式化的SIP消息转换成能用SOCK传输的简单数据并发送即完成消息发送,代码中有许多复杂的环境探测和错误控制等等等等,我们可以暂时不用过多关注,可以继续向下,结尾处有一个keeplive相关代码,从代码字面分析,可能是SIP的Register消息的自动重发相关代码,可以在后面再细化分析。   cb_tcp_snd_essage的函数实现要比上文的udp的实现简单很多,主要是环境探测错误控制方面,因为毕竟tcp是稳定连接的,对比一下代码,可以看到主要流程还是将SIP消息转换后,发送到从SIP消息中解析出的host和port对应的目标。      看完两个函数,可以知道,eXosip需要有两个sock,是一个数组,0是给UDP用的,1是给TCP用的,要用SOCK当然要初始化,就是下文要介绍的eXosip的网络相关的初始化了,上面的exosip_init可以看成是这个开发库的系统初始化吧:)    至些,我们应该知道了oSIP开发的SIP应用程序的消息是从哪里发出的吧,对了,就是从这个回调函数里,所谓万事开头难,就象开发WIN32应用程序时,找到了WIN32程序的main函数入口下面的工作就好办了,下面就都是为一些事件消息开发对应的处理函数而已了:)   osip_set_kill_transaction_callback 事务终结回调函数   对应ICT,IST,NICT,NIST客户/服务器注册/非注册事务状态机的终结,主要是使用osip_remove_transaction (eXosip.j_osip, tr)将当前tr事务删除,再加上一系列的清理工作,其中,NICT即客户端的非Invite事务的清理比较复杂一些,要处理的内容也比较多,可以根据实际应用的情况进行有必要的清理工作:)   cb_transport_error 传输失败处理回调   对应于上面说到的四种事务状态机,如果它们在处理时失败,则在这时进行统一处理。   从代码可知,只是在NOTIFY,SUBSCRIBE,OPTION操作失败才进行处理,其它错误可直接忽略。   osip_set_message_callback 消息发送处理回调   根据type不同,表示不同的消息发送状态   OSIP_XXX_AGAIN 重发相关消息   OSIP_ICT_INVITE_SENT 发起呼叫   OSIP_ICT_ACK_SENT ACK回应   OSIP_NICT_REGISTER_SENT 发起注册   OSIP_NICT_BYE_SENT BYE发出   OSIP_NICT_CANCEL_SENT Cancel发出   OSIP_NICT_INFO_SENT,OSIP_NICT_OPTIONS_SENT,OSIP_NICT_SUBSCRIBE_SENT,OSIP_NICT_NOTIFY_SENT,OSIP_NICT_UNKNOWN_REQUEST_SENT   我们可以看到,eXosip没有对它们作任何处理,我们可以根据自己需要,比如,重发2xx消息前记录一下日志之类的,扩展一下retransmission的处理方式,发起Invite前记录一下通话日志等等。   OSIP_ICT_STATUS_1XX_RECEIVED uac收到1xx消息,一般是表示对端正在处理中,这时,主要是设置一下事务状态机的状态值,并对会话中的osip的一些参数根据返回值进行相应设置,里面有许多条件判断,但我们常用的一般是100,180,183的判断而已,暂时可以忽略里面复杂的判断代码。   OSIP_ICT_STATUS_2XX_RECEIVED uac收到2xx消息,这里主要跟踪一下Register情况下的2xx,表示注册成功,这时会更新一下exosip的注册字段值,以便让eXosip能自动维护uac的注册,BYE的2xx回应是终结消息,Invite的2xx回应,则主要是初始化一下会话相关的数据,表示已成功建立连接。   其它4xx,5xx,6xx则分别是对应的处理,根据实现情况进行概要的查看即可。   report_event (je, sip)是代码中用来进行事件处理的一个函数,跟踪后发现,其最终是使用了我们上文提到的jpipe管道,以便在状态机外实时观测状态机内的处理信息。      OSIP_NIST_STATUS_XXX_SENT即对应于上面的uac的处理,这里是uas的对应的消息处理,相比较于uac简单一点。   前面简单介绍了一下大量的回调函数及它们的概要处理逻辑,可能会比较混乱,暂时不用管它,只需要记得一个大概的形象,知道一个SIP处理程序是通过osip_set_cb_send_message回调函数来实现真实地发送各种SIP消息,并且SIP的标准事务模型是由oSIP实现好了,我们只需要给不同的事务状态设置不同的回调处理函数来处理事务,具体的状态变化和内部逻辑不用管就可以了。   下面来说一下消息处理回调函数用到的SOCK的初始化函数,即我们上面说的除了系统初始化外的网络初始化函数eXosip_listen_addr:   从上文知道了,系统将初始化两个SOCK,一个UDP一个TCP,但查看代码发现还有第三个,TCPs的,但好象还不能实用,现在不管它,代码首先是根据传输是UDP还是TCP来设置对应的数组值,并且如果没有提供IP地址和端口号,系统会自动取出本机网络接口并创建可用的SOCK(http_port 的方式暂不用考虑)。   SOCK初始化后,如何开始SIP事务的呢?看到这个调用eXosip.j_thread = (void *) osip_thread_create (20000, _eXosip_thread, NULL),对的,这里启用了一个线程,即,eXosip是调用oSIP的线程函数(没用系统提供的线程函数,是为了跨平台)进行事务处理的状态机逻辑是在一个线程中处理的,这样就明白了为什么一直没能看到顺序执行下来的程序启动代码了,接下去看,线程实际处理函数是_eXosip_thread,这里面的代码中,我们看到了上文提到的状态机控制开关变量while (eXosip.j_stop_ua == 0),即,当j_stop_ua设置为1时,osip_thread_exit ()结束事务处理即程序终结,再接下去看,_eXosip_execute是最终的处理函数了,而且它在程序未终结情况下是一直逻辑在执行,注意,要启用oSIP的多线程宏OSIP_MT。      看到_eXosip_execute的代码中有很多时间函数和变量,仔细看,除去一些控制代码,主要处理函数是eXosip_read_message (1, lower_tv.tv_sec, lower_tv.tv_usec),即取出消息,1表示只取出一条消息,其代码量非常的大,但同样的,其中也许多的控制代码和错误检测代码,我们在查看时可以暂时忽略掉它们。   eXosip_read_message读取消息时,即没有采用sock的block也没有用非block方式,而是采用了select方式,具体应用可查询fd_set相关文档。   根据jpipe_read (eXosip.j_socketctl, buf2, 499),我们可以估计,buf2中应该是保存的我们的控制管道的数据,具体作用至些还没有表现出来,应该是用来反映一些状态机内部的警示之类的信息,实际的SIP的处理的状态机的数据是存放在buf中,使用_eXosip_recvfrom获取的,获取后sipevent = osip_parse (buf, i)解析,使用osip_find_transaction_and_add_event (eXosip.j_osip, sipevent)来查询事件对应的事务状态机,找到后就如同其注解所说明的,/* handled by oSIP ! */,即我们上文设置的那一大堆回调函数,至此,我们知道了整个SIP应用所处理的大概流程了。   如果没有找到事务状态机呢?直接丢弃吗?不是的,如果这是一个回应消息,但没有事务状态机处理它,那它是一个错误的,要进行清理后才能丢弃,而如果是一个请求,那更不能丢弃了,因为UAS事务状态机要由它来启动创建的(回应消息表示本地发出了请求消息,即UAC行为,事务状态机应是由启动UAC的代码初始化启动的),整个逻辑应该是很简单的,但eXosip的实现代码却非常多,可见其花了非常多的精力在保证会话的稳定性和应付网络复杂情况上,我们可以对其进行大量的精简来构建满足我们需求的代码实现。   先来看错误的回应消息的处理函数eXosip_process_response_out_of_transaction,可以看到其代码就是一大堆的赋值语句,XXX= NULL,即将一大堆的运行时变量清空,再调用osip_event_free清空事件,或者就是一些复杂的情况下,需要通过解析现在的运行时数据,从中分析出“可能”的正在等待回应的对端,并发送相关终结通知消息等等,可以根据实际需要进行简化。   请求事件的处理 eXosip_process_newrequest,首先是对事件进行探测,MSG_IS_INVITE、MSG_IS_ACK、 MSG_IS_REQUEST……,对事件进行所属状态机分类,随后使用_eXosip_transaction_init (&transaction,(osip_fsm_type_t) tx_type,eXosip.j_osip, evt->sip)根据探测结果进行状态机初始化,实际调用的是osip_transaction_init,初始化后即将事件入状态机 osip_transaction_add_event (transaction, evt),由状态机自动处理后调用相应回调函数处理逻辑了。当然,eXosip为方便快速开发SIP终端应用,在下面又添加了许多自动化的处理代码,来和我们在回调函数中设置的处理代码相区分。   线程调用的事件处理函数代码最后是 if (eXosip.keep_alive > 0) {   _eXosip_keep_alive (); }   这段代码印证了上文提到了,keep_alive是用来设置是否自动重新注册,由_eXosip_keep_alive函数来实现自动将eXosip全局变量中保存的注册消息解析后自动根据需要重新向SIP服务器发起Register注册。   同样,因为注册消息发起是UAC的行为,将它放在这里,可以看出来所有事件消息的事务状态机处理都是在这里,只不过这里只创建UAS的事务状态机,UAC的事务状态机的创建则要继续到下面找了,从我们的YouToo软电话代码中可知,发起呼叫和发起注册分别调用了 eXosip_call_send_initial_invite,eXosip_register_send_register这两个函数(另外用到的两个build函数则是分别构建这两个send函数要发送的SIP消息),查看这两个函数可知,UAC的事务处理状态机是在这里进行初始化的。   eXosip_register_send_register中可以看到是_eXosip_transaction_init (&transaction, NICT, eXosip.j_osip, reg)初始化UAC状态机,实际也同UAS是调用的osip_transaction_init函数,同样使用 osip_transaction_add_event (transaction, sipevent)将事件入状态机,状态机随后将自动处理调用相应回调函数处理逻辑了。   另有osip_new_outgoing_sipmessage(reg),表示发送消息,到这里,我们应该可以理解,真实的发送操作,是要到由状态机处理后,调用了消息发送回调函数才真正地将注册消息发送出去的。   同注册消息发送,它是NICT状态机,呼叫消息的发送是ICT,由eXosip_call_send_initial_invite处理,_eXosip_transaction_init (&transaction, ICT, eXosip.j_osip, invite)初始化了状态机,之前还有一个eXosip_call_init是用来初始化eXosip的一些参数的,暂时不管它,同样 osip_new_outgoing_sipmessage (invite)发送呼叫消息,但实际还是要状态机处理后调用消息发送回调函数真实发送呼叫请求函数的,osip_transaction_add_event (transaction, sipevent)则标准地,将事件入状态机,状态机将能处理随后的应用逻辑调用相应的回调函数了。   好了,作了这么多的分析,我们了解了eXosip是怎样调用oSIP来形成被我能方便地再次调用的了,可以看到,为了实现最大限度的跨平台和兼容性,代码中有大量的测试代码,宏定义和错误再处理代码,看起来非常吃力,但了解了其主要的调用框架:   初始化,回调函数设置,UAC和UAS事务处理状态机的启动,事件处理流程等,就可以基本明白了oSIP各个函数的主要作用和正确的用法了,下一步,可以参考eXosip来针对某个应用,去除掉大量暂时用不到的代码,来构建一个简单的SIP软电话和SIP服务器,来进一步深入oSIP学习应用了。  ------------------------------------------------------ [下回预告:完全基于oSIP的软电话实现及oSIP进一步学习] (CopyLeft by Meineson | www.mbstudio.cn,原创文章,欢迎转载,但请保留出处说明!) 附件为原作者提供的

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值