4.端点ENDPOINT

Endpoint类是一个单例类,应用程序必须在此类实例之前创建一个并且最多只能创建一个,然后才能执行任何操作。同样,一旦这个类被销毁,应用程序就不能调用该库的任何API。这个类是PJSUA2的核心类,它提供了以下功能:

  • 启动和关机
  • 配置的定制,如核心UA(用户代理)SIP配置,媒体配置和日志配置

本章将介绍上述功能。

要使用Endpoint类,通常应用程序不需要进行子类化(再写继承于该类的子类,简称子类化(subclass)),除非:

  • 应用程序希望实现/重载端点回调方法来获取如传输状态更改或NAT检测完成等事件,或者
  • 应用程序使用Endpoint.utilTimerSchedule()API调度计时器。在这种情况下,应用程序需要实现onTimer()回调以在定时器到期时获取通知。

4.1 实例化端点

在其他任何事情之前,必须实例化Endpoint类:

Endpoint *ep = new Endpoint;

一旦端点被实例化,可使用Endpoint.instance()静态方法获取端点实例。

4.2 创建库

通过调用其libCreate()方法来创建库:

try
{
ep->libCreate();
}
catch(Error& err)
{
cout << "Startup error: " << err.info() << endl;
}

如果发生错误,libCreate()方法将引发异常,因此我们需要使用try / catch子句来捕获异常。

4.3 初始化库并配置设置

EpConfig类提供端点配置,允许自定义以下设置:

  • UAConfig,指定核心SIP用户代理设置。
  • MediaConfig,指定各种媒体全局设置
  • LogConfig来自定义日志设置。

请注意,可以在AccountConfig中根据每个帐户进一步指定一些设置。

要自定义设置,请创建EpConfig类的实例,并在端点初始化期间指定它们(稍后将对此进行说明),例如:

EpConfig ep_cfg;
ep_cfg.logConfig.level = 5;
ep_cfg.uaConfig.maxCalls = 4;
ep_cfg.mediaConfig.sndClockRate = 16000;

接下来,通过调用libInit()初始化库:

try
{
EpConfig ep_cfg;
// Specify customization of settings in ep_cfg
ep->libInit(ep_cfg);
}
catch(Error& err)
{
cout << "Initialization error: " << err.info() << endl;
}

上面的代码片段使用默认设置初始化库。

4.4 创建一个或多个传输

在发送或接收SIP消息之前,应用程序需要创建一个或多个传输:

try 
{
TransportConfig tcfg;
tcfg.port = 5060;
TransportId tid = ep->transportCreate(PJSIP_TRANSPORT_UDP, tcfg);
}
catch(Error& err)
{
cout << "Transport creation error: " << err.info() << endl;
}

transportCreate()方法返回新创建的传输ID,它传递 传输类型和TransportConfig对象 来定制传输设置,如绑定地址和侦听端口号。没有这个,默认情况下,传输将绑定到INADDR_ANY和任何可用的端口。

除了创建无用户帐户(使用Account.create())以外,没有真正使用传输ID,稍后将对此进行说明),也许可以在应用程序想要的时候向用户显示传输列表。

4.5 创建安全传输(TLS)

要创建TLS传输,您可以使用与上述相同的方法。您可以通过修改字段TransportConfig.tlsConfig来进一步自定义TLS传输,例如设置证书文件或选择使用的密码。

try {
TransportConfig tcfg;
tcfg.port = 5061;
// Optional, set CA/certificate/private key files.
// tcfg.tlsConfig.CaListFile = "ca.crt";
// tcfg.tlsConfig.certFile = "cert.crt";
// tcfg.tlsConfig.privKeyFile = "priv.key";
// Optional, set ciphers. You can select a certain cipher/rearrange the order of ciphers here.
// tcfg.ciphers = ep->utilSslGetAvailableCiphers();
TransportId tid = ep->transportCreate(PJSIP_TRANSPORT_TLS, tcfg);
}
catch(Error& err)
{
cout << "Transport creation error: " << err.info() << endl;
}

4.5 启动库

现在我们启动库了。我们需要启动库完成初始化阶段,例如完成初始的STUN地址解析,初始化/启动声音设备等。要启动库,请调用libStart()方法:

try
{
ep->libStart();
}
catch(Error& err)
{
cout << "Startup error: " << err.info() << endl;
}

4.6 关闭库

应用程序退出后,库需要关闭,以便将资源释放回操作系统。虽然可以通过删除端点实例(内部调用libDestroy())来完成,最好是手动调用它,因为在Java或Python中,垃圾回收有问题,如前所述:

ep->libDestroy();
delete ep;

4.7 类引用

4.7.1 Endpoint

class pj::Endpoint

Endpoint (端点)表示pjsua库的一个实例

在应用程序中只能有一个pjsua库的实例,因此这个类是一个单例

公共功能(函数)

1)Endpoint()    //默认构造函数

2)virtual ~Endpoint()//虚析构函数

3)Version libVersion() const//得到库的版本

4)void libCreate()  //实例化pjsua应用程序。调用任何其他函数之前,应用程序必须调用此函数,以确保底层库被正确初始化。一旦此函数返回成功,应用程序必须在退出前调用libDestroy()

5)pjsua_state libGetState() const //获取库状态。返回库状态

6)void libInit(const EpConfig &prmEpConfig) //使用指定的设置初始化pjsua。所有设置都是可选的,并且在未指定配置时将使用默认值。请注意,在调用此函数之前必须调用create()。

  参数  prmEpConfig -端点配置

7)void libStart() // 在所有的初始化完成后,调用该函数,以便可以做其他检查设置。应用程序可以在init()之后的任何时间调用此函数。

8)void libRegisterThread(const string &name)//将由外部或自身API创建的线程注册到库。请注意,每次调用此函数时,它将分配一些内存来存储线程描述,这只会在库被销毁时被释放。

  参数name 要分配给线程的可选名称

9)bool libIsThreadRegistered()//检查该线程是否已经被注册到库中。需要注意的是此功能只适用于使用libRegisterThread()注册的库的主线程与工作线程和外部/自身线程。

10)void libStopWorkerThreads()//停止所有工作线程

11)int libHandleEvents(unsigned msec_timeout)//对pjsua进行事件轮询,如果需要,可以阻塞调用者线程指定的最大间隔(以毫秒为单位)。如果它在pjsua_config结构里配置了工作线程(thread_cnt域),应用程序通常不需要调用这个函数,因为轮询将由这些工作线程来完成。如果EpConfig :: UaConfig :: mainThreadOnly启用,并从主线程调用此函数(默认情况下,主线程是调用libCreate()的线程),此功能也将扫描并在列表中运行任何挂起的作业。

  返回  投票期间处理的事件数。负值表示错误,应用程序可以以(status = -return_value)方式检索错误。

  参数  msec_timeout - 最长时间等待,以毫秒为单位。

12)void libDestroy(unsigned prmFlags = 0) //销毁pjsua. 建议使用应用程序在调用此功能之前执行正常shutdown(例如从SIP服务器注销帐户,终止预订订阅和挂断主动调用).但是如果发现有活动会话,此函数将执行所有这些操作以终止活动会话。此功能将等待(block)几秒钟等待远程的回复。如果没有跟踪它的状态,Application.可以多次安全地调用此函数。

  参数  prmFlags,  pjsua_destroy_flag 枚举值的组合

string utilStrError(pj_status_t prmErr) 检索指定状态代码的错误字符串。

参数 prmErr -错误代码。

13)void utilLogWrite(int prmLevelconst string &prmSenderconst string &prmMsg)写一个日志消息

参数 prmLevel -日志详细程度(1-5)

prmSender -日志发送方。

prmMsg -日志消息。

14)void utilLogWrite(LogEntry &e) 写一个日志条目。

参数 e - 日志条目

15)pj_status_t utilVerifySipUri(const string &prmUri) 这是一个通用用函数,用于验证是否给出了有效的SIP URL。如果URL是有效的SIP / SIPS方案,则将返回PJ_SUCCESS

返回 PJ_SUCCESS成功,或相应的错误代码

参考 utilVerifyUri()

16)pj_status_t utilVerifyUri(const string &prmUri) 这是一个通用用函数,用于验证是否给出了有效的URL。与utilVerifySipUri()不同,如果给出tel:URI,此函数将返回PJ_SUCCESS

返回 PJ_SUCCESS成功,或相应的错误代码

PJ_SUCCESS on success, or the appropriate error code.

参考 pjsua_verify_sip_url()

参数 prmUri - URL字符串

17)Token utilTimerSchedule(unsigned prmMsecDelayToken prmUserData)

设定具有指定间隔和用户数据的定时器。当间隔时间到时,将调用onTimer()回调。注意,回调可能由不同的线程执行,具体取决于是否启用了工作线程

返回 令牌识别定时器,可以用utilTimerCancel()取消定时器

参数 prmMsecDelay -时间间隔,单位为毫秒

prmUserData - 任意用户数据,被回馈给应用程序在回调。

18)void utilTimerCancel(Token prmToken) 使用指定的定时器令牌取消先前指定的定时器

参数 prmToken -从以前的utilTimerSchedule()调用返回的计时器令牌。

19)void utilAddPendingJob(PendingJob *job)

注册要由主线程执行的待处理作业的实用程序。如果EpConfig :: UaConfig :: mainThreadOnly为false,该作业将立即执行。

参数 job 工作类

20)IntVector utilSslGetAvailableCiphers() 获取SSL / TLS后端支持的密码列表。

21)void natDetectType(void) 这是一个在这个端点前面检测NAT类型的通用函数,一旦成功调用,此函数将异步完成,并在onNatDetectionComplete()中报告结果.在检测到NAT并调用回调后,应用程序可以通过调用natGetType()获取检测到的NAT类型。应用程序还可以在稍后再次调用natDetectType()来执行NAT检测。注意,必须启用STUN才能成功运行此功能。

22)pj_stun_nat_type natGetType() 获取natDetectType()函数检测到的NAT类型。

natDetectType()已成功完成并且已调用onNatDetectionComplete()回调函数后,此函数将仅返回有用的NAT类型。

异常:如果在检测过程中调用此函数,将引发PJ_EPENDING异常。

23)void natUpdateStunServers(const StringVector &prmServers, bool prmWait)更新STUN服务器列表。libInit()必须在调用这个函数之前调用。

参数prmServers - STUN服务器数组尝试。端点将尝试解析并联系每个STUN服务器条目,直到找到可用的条目。每个条目可能是域名,主机名,IP地址,并且可能包含可选的端口号。例如:

“pjsip.org”(域名)

“sip.pjsip.org”(主机名)

“pjsip.org:33478”(域名和非标准端口号)

“10.0.0.1:3478”(IP地址和端口号)

prmWait - 指定函数是否应该阻塞,直到得到结果。在这种情况下,函数将在分辨率完成时阻塞,并且在该函数返回之前调用onNatCheckStunServersComplete()

24)void natCheckStunServers(const StringVector &prmServers, bool prmWaitTokenprmUserData)  辅助函数,用于解析和联系每个STUN服务器条目(依次)以查找哪个可用。须先调用libInit()

  参考   natCancelCheckStunServers()

  参数 prmServers - STUN服务器数组尝试。端点将尝试解析并联系每个STUN服务器条目,直到找到可用的条目。每个条目可能是域名,主机名,IP地址,并且可能包含可选的端口号。例如:

“pjsip.org”(域名)

“sip.pjsip.org”(主机名)

“pjsip.org:33478”(域名和非标准端口号)

“10.0.0.1:3478”IP地址和端口号)

 prmWait - 指定函数是否应该阻塞,直到得到结果。在这种情况下,函数将在分辨率完成时阻塞,并且在该函数返回之前调用回调函数。prmUserData - 任意用户数据要在回调中传回应用程序。

25)void natCancelCheckStunServers(Token token, bool notify_cb = false)取消与指定令牌匹配的待决STUN分辨率。

异常:如果没有匹配的PJ_ENOTFOUND或其他错误。

参数token -令牌匹配。 这个令牌给了natCheckStunServers()

notify_cb -布尔值,用于控制是否为已取消的方案调用回调。 当调用回调时,结果中的状态将被设置为PJ_ECANCELLED。

26)TransportId transportCreate ( pjsip_transport_type_e type , const TransportConfig & cfg ) 根据指定的设置创建并启动新的SIP传输。

返回传输ID。

参数type -传输类型。cfg -传输配置。

27)IntVector transportEnum ( )

枚举系统中当前创建的所有传输。

此函数将返回所有传输ID,然后应用程序可以调用transportGetInfo()函数来检索有关传输的详细信息。

返回 传输ID数组。

28)TransportInfo transportGetInfo ( TransportId id )

获取有关传输的信息。

返回 交通信息

参数 id - 运输ID。

29)void transportSetEnable ( TransportId id ,bool enabled )

禁用传输或重新启用它。

默认情况下,传输始终在创建后启用。 禁用传输不一定关闭套接字,它只会丢弃传入的消息,并阻止传输被用于发送传出的消息。

参数id -传输ID。

enabled -启用或禁用传输。

30)void transportClose ( TransportId id ) 关闭传输。

系统将等待所有事务关闭,同时防止新用户使用传输,并在其使用次数达到零时关闭传输。

参数id -传输ID。

31)void transportShutdown ( TransportHandle tp )

启动此传输句柄的正常关闭程序。

在正常关闭初始化后,传输获取不到新的引用。 然而,当前使用传输的现有对象仍然可以使用该传输来发送和接收数据包。 所有的对象释放对该引用后传输将立即销毁。

注意:从回调onTransportState()获取句柄后,应用程序通常会使用此API。

参数tp -传输句柄。

32)void hangupAllCalls ( void )

终止所有呼叫

这将启动所有当前活动呼叫的呼叫挂断。

33)void mediaAdd ( AudioMedia & media )

将媒体添加到媒体列表。

参数media -要添加的媒体

34)void mediaRemove ( AudioMedia & media )

从媒体列表中删除媒体。

参数media -要移除的媒体

35)bool mediaExists ( const AudioMedia & media ) const

检查媒体是否已添加到媒体列表。

返回 如果添加了媒体,则为真,否则为false。

参数media -要检查的媒体

36)unsigned  mediaMaxPorts ( ) mediaMaxPorts获取媒体端口的最大数量

返回 会议桥中媒体端口的最大数量。

37)unsigned  mediaActivePorts ( ) mediaActivePorts

获取桥活性介质端口的当前数目

返回会议桥中媒体端口的最大数量

38)const AudioMediaVector & mediaEnumPorts ( ) mediaEnumPorts

枚举所有媒体端口。

返回 媒体端口列表。

39)AudDevManager & audDevManager ( )

获取音频设备管理器的实例。

返回 音频设备管理器。

40)VidDevManager & vidDevManager ( )

获取视频设备管理器的实例。

返回 视频设备管理器。

41)const CodecInfoVector & codecEnum ( )

枚举所有支持的编解码器在系统中。

返回 编解码器信息数组。

42)void codecSetPriority ( const string& codec_id ,pj_uint8_t priority )

更改编解码优先级

参数codec_id -编解码器ID,它是唯一标识编解码器的字符串(如“speex / 8000”)。

priority -编解码器优先级0-255,其中0表示禁用编解码器。

43)CodecParam codecGetParam ( const string& codec_id ) const¶

获取编解码器参数

返回 编解码器参数。 如果没有找到编解码器,则会抛出错误 。

参数codec_id - 编解码器ID。

44)void codecSetParam ( const string& codec_id , const CodecParam param )

设置编解码器参数

参数 codec_id - 编解码器ID。

param -编解码器参数设置。 设置为NULL将编解码器参数重置为库默认设置。

45)const CodecInfoVector & videoCodecEnum ( )

枚举所有支持的视频编解码器在系统中。

返回  视频编解码器信息数组。

46)void videoCodecSetPriority ( const string& codec_id ,pj_uint8_t priority )

更改视频编解码优先级

参数codec_id - 编解码器ID,其是唯一标识编解码器的字符串(例如“H263 / 90000”)。 有关详细信息,请参阅pjsua手册或pjmedia编解码器参考。

priority - 编解码器优先级0-255,其中0表示禁用编解码器。

47)VidCodecParam getVideoCodecParam ( const string& codec_id ) const¶

获取视频编解码器参数。

返回

编解码器参数。 如果没有找到编解码器,则会抛出错误 。

参数

  • codec_id -

编解码器ID。

48)void setVideoCodecParam ( const string& codec_id , const VidCodecParam & param )

设置视频编解码器参数。

参数

  • codec_id -

编解码器ID。

  • param -

编解码器参数设置。

49)void resetVideoCodecParam ( const string& codec_id )

将视频编解码器参数重置为库默认设置。

参数

  • codec_id -

编解码器ID。

50)virtual void onNatDetectionComplete ( const OnNatDetectionCompleteParam & prm )

端点完成使用natDetectType()启动的NAT类型检测时的回调。

参数

  • prm -

包含检测结果的回调参数。

51)virtual  void onNatCheckStunServersComplete ( const OnNatCheckStunServersCompleteParam & prm )

Endpoint完成执行调用libInit()或调用natCheckStunServers()natUpdateStunServers()时启动的STUN服务器检查时的回调。

参数prm -回调参数

52)virtual  void onTransportState ( const OnTransportStateParam & prm )

传输状态发生变化时调用此回调。

参数prm -回调参数

53)virtual  void onTimer ( const OnTimerParam & prm )

计时器触发时回调。

计时器由utilTimerSchedule()调度 。

参数prm -回调参数

54)virtual  void onSelectAccount ( OnSelectAccountParam & prm )

应用程序可以使用此回调来覆盖用于处理传入消息的帐户。

最初,使用的帐户将由图书馆自动计算。 如果应用程序没有实现此回调,则该初始帐户将被使用,或者从此回调返回时应用程序设置无效的帐户。

请注意,目前需要帐号分配的传入消息是INVITE,MESSAGE,SUBSCRIBE和未经请求的NOTIFY。 这个回调可以在SIP事件本身的回叫之前被调用,即:来电,寻呼机,订阅或者非请求事件。

参数 prm - 回调参数

 公共静态功能

55)static Endpoint & instance ( )

检索端点的单例实例。

4.7.2 端点配置

Endpoint
struct pj::EpConfig
#include <endpoint.hpp>

endpoint配置

公共功能

  void readObject(const ContainerNode&node)

    从容器读取此对象。

    参数

    •node - 要从中写入值的容器。

  void writeObject(ContainerNode&node)

    将此对象写入容器。

    参数

      •node - 要将值写入的容器。

公有成员

  UaConfig uaConfig

    UA配置

  LogConfig logConfig

    记录配置。

  MediaConfig medConfig

    媒体配置

媒体

struct pj::MediaConfig

此结构描述媒体配置,在调用Lib :: init()时指定。

pj :: PersistentObject继承

记录

struct pj::LogConfig

记录配置,可以(可选)在调用Lib :: init()时指定。

pj :: PersistentObject继承

class pj::LogWriter

用于编写日志消息的界面

应用程序可以继承此类并在LogConfig结构中提供它,以实现自定义日志写入工具。

公共函数

virtual ~LogWriter()

析构器

virtual void writeconst LogEntryentry ) = 

写一个日志条目。

struct pj::LogEntry

包含由LogWriter写入的日志条目的数据。

用户代理

struct pj::UaConfig

SIP用户代理相关设置。

pj :: PersistentObject继承

回调参数

struct pj::OnNatDetectionCompleteParam

Endpoint :: onNatDetectionComplete()回调的参数。

struct pj::OnNatCheckStunServersCompleteParam

Endpoint :: onNatCheckStunServersComplete()回调的参数。

struct pj::OnTimerParam

端点::的OnTimer()回调的参数

struct pj::OnTransportStateParam

端点:: onTransportState()回调的参数

struct pj::OnSelectAccountParam

端点:: onSelectAccount()回调的参数

其他

struct pj::PendingJob

PJSUA2开发文档--第四章 端点ENDPOINT的更多相关文章

  1. PJSUA2开发文档--第五章 帐户(号)Accounts

    第五章 帐户(号) 帐户提供正在使用该应用程序的用户的身份(或身份).一个帐户有一个与之相关的SIP统一资源标识符(URI).在SIP术语中,该URI用作该人的记录地址( Address of Rec ...

  2. PJSUA2开发文档--第三章 PJSUA2高级API

    3. PJSUA2高级API PJSUA2是PJSUA API以上的面向对象抽象.它为构建会话发起协议(SIP)多媒体用户代理应用程序(也称为IP / VoIP软电话)提供高级API.它将信令,媒体和 ...

  3. PJSUA2开发文档--第七章 呼叫 Calls类

    7   呼叫Calls 呼叫由Call类处理 7.1 子类化Call类 要使用Call类,应用程序应创建子类,如: class MyCall : public Call { public: MyCal ...

  4. PJSUA2开发文档--第六章 媒体 Media类

    6. 媒体(Media) 媒体对象是能够产生媒体或接受媒体的对象. Media的重要子类是AudioMedia,它代表音频媒体.PJSUA2支持多种类型的音频媒体对象: 捕获设备的AudioMedia ...

  5. PJSUA2开发文档--第十一章 网络问题

    11 网络问题 11.1 IP地址更改 请参阅wiki 处理IP地址更改.请注意,本指南使用PJSUA API作为参考. 11.2 被阻止/过滤的网络 请参阅维基百科 通过阻止或过滤的VoIP网络

  6. PJSUA2开发文档--第十二章 PJSUA2 API 参考手册

    12 PJSUA2 API 参考手册 12.1 endpoint.hpp PJSUA2基本代理操作.  namespace pj PJSUA2 API在pj命名空间内. 12.1.1 class En ...

  7. PJSUA2开发文档--第九章 PJSUA2应用程序示例

    9. PJSUA2示例应用程序 9.1 示例应用程序 9.1.1 C++ pjsip-apps/src/samples/pjsua2_demo.cpp 是一个非常简单可用的C++示例应用程序. /* ...

  8. PJSUA2开发文档--第八章 好友(Buddy)类

    8  好友(存在)Buddy PJSUA2的功能是围绕Buddy类为中心展开的.该类表示一个远端好友(伙伴,一个人或一个SIP端点). 8.1 子类化Buddy类 要使用Buddy类,通常应创建子类, ...

  9. PJSUA2开发文档--第十章 媒体质量(MEDIA QUALITY)

    10 媒体质量(Media Quality) 10.1 音频质量 如果遇到音频质量问题,可尝试以下步骤: 遵循指南:使用pjsystest测试声音设备. 识别声音问题并使用以下步骤进行故障排除:检查声 ...

随机推荐

  1. [Swift]LeetCode521. 最长特殊序列 Ⅰ | Longest Uncommon Subsequence I

    Given a group of two strings, you need to find the longest uncommon subsequence of this group of two ...

  2. Hystrix概念设计

    1. Hystrix概念设计 1.1. 大纲 1.2. 基本的容错模式 1.3. 断路器模式 1.4. 舱壁隔离模式 1.5. 容错理念 凡事依赖都可能失败 凡事资源都有限制 网络并不可靠 延迟是应用 ...

  3. Linux(Ubuntu) OpenGL 开发环境

    Linux(Ubuntu) OpenGL 开发环境 在 PC 平台上开发 OpenGL 可以使用的辅助工具有很多选择,这里我主要参考了 learnopengl 的配置,使用 GLFW 和 GLAD. ...

  4. PHP的Memcached简单实现

    Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度.也可动态缓存一些实 ...

  5. Java货币金额转换为大写形式

    package com.test; import java.math.BigDecimal; /** * * * 数字转换为汉语中人民币的大写<br> * */ public class ...

  6. BBS论坛(七)

    7.1.修改邮箱界面完成 (1)cms/cms_resetemail.html {% extends 'cms/cms_base.html' %} {% block title -%} 修改邮箱 {% ...

  7. 设计模式的征途—4.抽象工厂(Abstract Factory)模式

    上一篇的工厂方法模式引入了工厂等级结构,解决了在原来简单工厂模式中工厂类职责太重的原则,但是由于工厂方法模式的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,从而增加系统开销.那么,我们应该 ...

  8. 用Maven快速生成带有依赖的可执行jar包

    一.背景 最近项目在做微服务的拆分,那么我们想让我们的容器启动更加的轻量级,所以我们选择放弃tomcat等容器,而是通过maven生成带有指定依赖的可执行jar包的方式进行处理,本文我将分享如何通过m ...

  9. CentOS安装Java JDK

    JDK是 Java 语言的软件开发工具包,主要用于移动设备.嵌入式设备上的java应用程序.在Linux上安装Tomcat,而Tomcat服务器运行时是需要JDK支持的,所以服务器必须配置好JDK用到 ...

  10. [Leetcode]237. Delete Node in a Linked List -David_Lin

    Write a function to delete a node (except the tail) in a singly linked list, given only access to th ...