BREDR的设备 在进行配对完成之后,进行;连接之前都要进行服务的搜索,服务搜索走的流程是SDP,这篇文章就分析一下,bluedroid中SDP的代码流程,我们从配对完成的回调函数开始分析:

/*******************************************************************************
**
** Function btif_dm_auth_cmpl_evt
**
** Description Executes authentication complete event in btif context
**
** Returns void
**
*******************************************************************************/
static void btif_dm_auth_cmpl_evt (tBTA_DM_AUTH_CMPL *p_auth_cmpl)
{
/* Save link key, if not temporary */
bt_bdaddr_t bd_addr;
bt_status_t status = BT_STATUS_FAIL;
bt_bond_state_t state = BT_BOND_STATE_NONE;
BOOLEAN skip_sdp = FALSE;
...
if (p_auth_cmpl->success)
{
btif_update_remote_properties(p_auth_cmpl->bd_addr,
p_auth_cmpl->bd_name, NULL, p_auth_cmpl->dev_type);
pairing_cb.timeout_retries = ;
status = BT_STATUS_SUCCESS;
state = BT_BOND_STATE_BONDED;
bdcpy(bd_addr.address, p_auth_cmpl->bd_addr); if (check_sdp_bl(&bd_addr) && check_cod_hid(&bd_addr, COD_HID_MAJOR))
{
LOG_WARN("%s:skip SDP", __FUNCTION__);
skip_sdp = TRUE;
}
if(!pairing_cb.is_local_initiated && skip_sdp)
{
...
}
else
{
/* Trigger SDP on the device */
pairing_cb.sdp_attempts = ;;
...
if(btif_dm_inquiry_in_progress)
btif_dm_cancel_discovery(); btif_dm_get_remote_services(&bd_addr);//进行服务搜索
}
// Do not call bond_state_changed_cb yet. Wait until remote service discovery is complete
}
...

我这里分析的设备是音箱,会直接走SDP的流程。也就是会执行上面的btif_dm_get_remote_services函数,这里注意一点就是,什么时候上报配对的状态?从上面的注释可以看到,bluedroid 并不会在配对完成就上报配对的状态,而是要等服务搜索完成。我们继续分析,服务搜索的流程:

/*******************************************************************************
**
** Function btif_dm_get_remote_services
**
** Description Start SDP to get remote services
**
** Returns bt_status_t
**
*******************************************************************************/
bt_status_t btif_dm_get_remote_services(bt_bdaddr_t *remote_addr)
{
bdstr_t bdstr; BTA_DmDiscover(remote_addr->address, BTA_ALL_SERVICE_MASK,
bte_dm_search_services_evt, TRUE); return BT_STATUS_SUCCESS;
}

调用到BTA的BTA_DmDiscover,将搜索的 指令开始往下发,其第一个参数是设备的地址,第二个参数是BTA_ALL_SERVICE_MASK = 0x3fffffff,第三个参数是回调,这样的回调我们见的太多,它的命名格式也是大致相同:bte_XXX_evt,

最后一个参数代表是否进行SDP 搜索,这里是true,这个函数实现在bta_dm_api.c里面,这里的函数,十有八九都是组建一个msg,然后调用bta_sys_sendmsg 把消息发送到BTU task 来处理,我们继续分析:

/*******************************************************************************
**
** Function BTA_DmDiscover
**
** Description This function does service discovery for services of a
** peer device
**
**
** Returns void
**
*******************************************************************************/
void BTA_DmDiscover(BD_ADDR bd_addr, tBTA_SERVICE_MASK services,
tBTA_DM_SEARCH_CBACK *p_cback, BOOLEAN sdp_search)
{
tBTA_DM_API_DISCOVER *p_msg; if ((p_msg = (tBTA_DM_API_DISCOVER *) GKI_getbuf(sizeof(tBTA_DM_API_DISCOVER))) != NULL)
{
memset(p_msg, , sizeof(tBTA_DM_API_DISCOVER)); p_msg->hdr.event = BTA_DM_API_DISCOVER_EVT;//discover msg
bdcpy(p_msg->bd_addr, bd_addr);
p_msg->services = services;
p_msg->p_cback = p_cback;
p_msg->sdp_search = sdp_search;
bta_sys_sendmsg(p_msg);
}
}

这里向BTU task 发送BTA_DM_API_DISCOVER_EVT :(BTA got event 0x202)

这里device manager search (BTA_ID_DM_SEARCH)注册到系统的处理句柄 是:

static const tBTA_SYS_REG bta_dm_search_reg =
{
bta_dm_search_sm_execute,
bta_dm_search_sm_disable
};

我们继续看:

/*******************************************************************************
**
** Function bta_dm_search_sm_execute
**
** Description State machine event handling function for DM
**
**
** Returns void
**
*******************************************************************************/
BOOLEAN bta_dm_search_sm_execute(BT_HDR *p_msg)
{
tBTA_DM_ST_TBL state_table;
UINT8 action;
int i; APPL_TRACE_EVENT("bta_dm_search_sm_execute state:%d, event:0x%x",
bta_dm_search_cb.state, p_msg->event); /* look up the state table for the current state */
state_table = bta_dm_search_st_tbl[bta_dm_search_cb.state]; bta_dm_search_cb.state = state_table[p_msg->event & 0x00ff][BTA_DM_SEARCH_NEXT_STATE]; /* execute action functions */
for (i = ; i < BTA_DM_SEARCH_ACTIONS; i++)
{
if ((action = state_table[p_msg->event & 0x00ff][i]) != BTA_DM_SEARCH_IGNORE)
{
(*bta_dm_search_action[action])( (tBTA_DM_MSG*) p_msg);
}
else
{
break;
}
}
return TRUE;
}

状态机轮转,一看也是熟悉的套路。我们看看状态的转换吧,一开始肯定是idle 的状态:bta_dm_search_idle_st_table:

/* API_SEARCH_DISC */       {BTA_DM_API_DISCOVER,              BTA_DM_SEARCH_IGNORE,          BTA_DM_DISCOVER_ACTIVE},

下一个状态是BTA_DM_DISCOVER_ACTIVE,执行的动作是BTA_DM_API_DISCOVER,执行的函数:

/*******************************************************************************
**
** Function bta_dm_discover
**
** Description Discovers services on a remote device
**
**
** Returns void
**
*******************************************************************************/
void bta_dm_discover (tBTA_DM_MSG *p_data)
{
/* save the search condition */
bta_dm_search_cb.services = p_data->discover.services; bta_dm_search_cb.p_search_cback = p_data->discover.p_cback;//前面注册callbak = bte_dm_search_services_evt
bta_dm_search_cb.sdp_search = p_data->discover.sdp_search;//true
bta_dm_search_cb.services_to_search = bta_dm_search_cb.services;//0x3fffffff
bta_dm_search_cb.service_index = ;
bta_dm_search_cb.services_found = ;
bta_dm_search_cb.peer_name[] = ;
bta_dm_search_cb.sdp_search = p_data->discover.sdp_search;//bluedroid 竟然执行了两遍
bta_dm_search_cb.p_btm_inq_info = BTM_InqDbRead (p_data->discover.bd_addr);//查找数据库
bta_dm_search_cb.transport = p_data->discover.transport; bta_dm_search_cb.name_discover_done = FALSE;//标志 名字搜索没有完成
memcpy(&bta_dm_search_cb.uuid, &p_data->discover.uuid, sizeof(tSDP_UUID));
bta_dm_discover_device(p_data->discover.bd_addr);//开始搜索
}

下面我们接着分析bta_dm_discover_device:

/*******************************************************************************
**
** Function bta_dm_discover_device
**
** Description Starts name and service discovery on the device
**
** Returns void
**
*******************************************************************************/
static void bta_dm_discover_device(BD_ADDR remote_bd_addr)
{
tBTA_DM_MSG * p_msg;
tBT_TRANSPORT transport = BT_TRANSPORT_BR_EDR; /* Reset transport state for next discovery */
bta_dm_search_cb.transport = BTA_TRANSPORT_UNKNOWN;//这里为啥这样设置?看上面注释,为了下一次的搜索。这一次的搜索因为知道了transport bdcpy(bta_dm_search_cb.peer_bdaddr, remote_bd_addr);
... /* if name discovery is not done and application needs remote name */
if ((!bta_dm_search_cb.name_discover_done)
&& (( bta_dm_search_cb.p_btm_inq_info == NULL )
||(bta_dm_search_cb.p_btm_inq_info && (!bta_dm_search_cb.p_btm_inq_info->appl_knows_rem_name))))
{
if (bta_dm_read_remote_device_name(bta_dm_search_cb.peer_bdaddr, transport) == TRUE)//如果需要discovery name ,那么将继续进行
return; /* starting name discovery failed */
bta_dm_search_cb.name_discover_done = TRUE;
}
APPL_TRACE_DEBUG("bta_dm_search_cb.services %d libs_liu", bta_dm_search_cb.services); /* if application wants to discover service */
if ( bta_dm_search_cb.services )//0x3fffffff
{
/* initialize variables */
bta_dm_search_cb.service_index = ;
bta_dm_search_cb.services_found = ;
bta_dm_search_cb.services_to_search = bta_dm_search_cb.services;
...
/* if seaching with EIR is not completed */
if(bta_dm_search_cb.services_to_search)
{
/* check whether connection already exists to the device
if connection exists, we don't have to wait for ACL
link to go down to start search on next device */
if (BTM_IsAclConnectionUp(bta_dm_search_cb.peer_bdaddr, BT_TRANSPORT_BR_EDR))
bta_dm_search_cb.wait_disc = FALSE;
else
bta_dm_search_cb.wait_disc = TRUE;
...
{
bta_dm_search_cb.sdp_results = FALSE;
bta_dm_find_services(bta_dm_search_cb.peer_bdaddr);//继续向下传达搜索指令
return;
}
}
} /* name discovery and service discovery are done for this device */
...
}

我们直接看bta_dm_find_services:这里是真正的搜索的逻辑:

/*******************************************************************************
**
** Function bta_dm_find_services
**
** Description Starts discovery on a device
**
** Returns void
**
*******************************************************************************/
static void bta_dm_find_services ( BD_ADDR bd_addr)
{ tSDP_UUID uuid;
UINT16 num_attrs = ;
tBTA_DM_MSG *p_msg; memset (&uuid, , sizeof(tSDP_UUID)); while(bta_dm_search_cb.service_index < BTA_MAX_SERVICE_ID)//32,涉及很多的sevice,gatt、source、sink、hsp、hfp、map等
{
if( bta_dm_search_cb.services_to_search
& (tBTA_SERVICE_MASK)(BTA_SERVICE_ID_TO_SERVICE_MASK(bta_dm_search_cb.service_index)))//搜索所有的服务
{
if((bta_dm_search_cb.p_sdp_db = (tSDP_DISCOVERY_DB *)GKI_getbuf(BTA_DM_SDP_DB_SIZE)) != NULL)
{
/* try to search all services by search based on L2CAP UUID */
if(bta_dm_search_cb.services == BTA_ALL_SERVICE_MASK )
{
if (bta_dm_search_cb.services_to_search & BTA_RES_SERVICE_MASK)/*reserved,id = 1,这里发现了其处理和其他的service不一样*/
{
uuid.uu.uuid16 = bta_service_id_to_uuid_lkup_tbl[];//这个reserved 服务就是UUID_SERVCLASS_PNP_INFORMATION
bta_dm_search_cb.services_to_search &= ~BTA_RES_SERVICE_MASK;//消除掉这个标志位,下次就会进入到else 流程
}
else
{
uuid.uu.uuid16 = UUID_PROTOCOL_L2CAP;//搜索所有基于l2cap的服务
bta_dm_search_cb.services_to_search = ;//注意这里设置为0,
}
}
else
{
...
} if (uuid.len == )
uuid.len = LEN_UUID_16; if (bta_dm_search_cb.service_index == BTA_USER_SERVICE_ID)
{
memcpy(&uuid, &bta_dm_search_cb.uuid, sizeof(tSDP_UUID));
} SDP_InitDiscoveryDb (bta_dm_search_cb.p_sdp_db, BTA_DM_SDP_DB_SIZE, , &uuid, , NULL);//对SDP的数据库进行初始化 memset(g_disc_raw_data_buf, , sizeof(g_disc_raw_data_buf));
bta_dm_search_cb.p_sdp_db->raw_data = g_disc_raw_data_buf; bta_dm_search_cb.p_sdp_db->raw_size = MAX_DISC_RAW_DATA_BUF; if (!SDP_ServiceSearchAttributeRequest (bd_addr, bta_dm_search_cb.p_sdp_db, &bta_dm_sdp_callback))//调用到sdp_api.c里面的接口
{
/* if discovery not successful with this device
proceed to next one */
GKI_freebuf(bta_dm_search_cb.p_sdp_db);
bta_dm_search_cb.p_sdp_db = NULL;
bta_dm_search_cb.service_index = BTA_MAX_SERVICE_ID; }
else
{
bta_dm_search_cb.service_index++;
return;//直接返回,这里猜想,前一次搜索结果回来之后,应该还是会触发下一次继续进行搜索
}
}
else
{
APPL_TRACE_ERROR("#### Failed to allocate SDP DB buffer! ####");
}
} bta_dm_search_cb.service_index++;
} /* no more services to be discovered *///全部搜索完成会发送BTA_DM_DISCOVERY_RESULT_EVT
if(bta_dm_search_cb.service_index >= BTA_MAX_SERVICE_ID)
{
if ((p_msg = (tBTA_DM_MSG *) GKI_getbuf(sizeof(tBTA_DM_MSG))) != NULL)
{
p_msg->hdr.event = BTA_DM_DISCOVERY_RESULT_EVT;
p_msg->disc_result.result.disc_res.services = bta_dm_search_cb.services_found;
bdcpy (p_msg->disc_result.result.disc_res.bd_addr, bta_dm_search_cb.peer_bdaddr);
BCM_STRNCPY_S((char*)p_msg->disc_result.result.disc_res.bd_name, sizeof(BD_NAME),
bta_dm_get_remname(), (BD_NAME_LEN-)); /* make sure the string is terminated */
p_msg->disc_result.result.disc_res.bd_name[BD_NAME_LEN-] = ; bta_sys_sendmsg(p_msg);
}
}
}

从上面的流程,我们可以知道,它是首先进行UUID_SERVCLASS_PNP_INFORMATION的搜索,然后再进行所有基于L2cap的服务的搜索,同时将bta_dm_search_cb.services_to_search设置为0,当所有的服务搜索完成之后,会上报BTA_DM_DISCOVERY_RESULT_EVT 进行处理。那大概的流程,我们心理已经基本清楚,我们再继续详细分析其流程:

我们首先分析SDP_ServiceSearchAttributeRequest的代码流程:

/*******************************************************************************
**
** Function SDP_ServiceSearchAttributeRequest
**
** Description This function queries an SDP server for information.
**
** The difference between this API function and the function
** SDP_ServiceSearchRequest is that this one does a
** combined ServiceSearchAttributeRequest SDP function.
** (This is for Unplug Testing)
**
** Returns TRUE if discovery started, FALSE if failed.
**
*******************************************************************************/
BOOLEAN SDP_ServiceSearchAttributeRequest (UINT8 *p_bd_addr, tSDP_DISCOVERY_DB *p_db,
tSDP_DISC_CMPL_CB *p_cb)
{
#if SDP_CLIENT_ENABLED == TRUE
tCONN_CB *p_ccb; /* Specific BD address */
p_ccb = sdp_conn_originate (p_bd_addr);//建立sdp 的channel if (!p_ccb)
return(FALSE); p_ccb->disc_state = SDP_DISC_WAIT_CONN;
p_ccb->p_db = p_db;
p_ccb->p_cb = p_cb; p_ccb->is_attr_search = TRUE; return(TRUE);
#else
return(FALSE);
#endif
}

这里发现,每次SDP_ServiceSearchAttributeRequest 的时候都会执行sdp_conn_originate,那我们也可以预期,但是这一次的SDP的搜索行为结束(收到response),应该就会把这个channel 断开,从log 中也确实是如此的。

下面我们继续看看sdp_conn_originate的实现:

/*******************************************************************************
**
** Function sdp_conn_originate
**
** Description This function is called from the API to originate a
** connection.
**
** Returns void
**
*******************************************************************************/
tCONN_CB* sdp_conn_originate (UINT8 *p_bd_addr)
{
tCONN_CB *p_ccb;
UINT16 cid; /* Allocate a new CCB. Return if none available. */
if ((p_ccb = sdpu_allocate_ccb()) == NULL)//在sdp_cb.ccb中分配新的channel cb,sdp_cb里面保存了最大ccb是4
{
SDP_TRACE_WARNING ("SDP - no spare CCB for orig");
return (NULL);
} /* We are the originator of this connection */
p_ccb->con_flags |= SDP_FLAGS_IS_ORIG; /* Save the BD Address and Channel ID. */
memcpy (&p_ccb->device_address[], p_bd_addr, sizeof (BD_ADDR)); /* Transition to the next appropriate state, waiting for connection confirm. */
p_ccb->con_state = SDP_STATE_CONN_SETUP; cid = L2CA_ConnectReq (SDP_PSM, p_bd_addr);//为这次transaction 建立l2cap 连接 /* Check if L2CAP started the connection process */
if (cid != )
{
p_ccb->connection_id = cid; return (p_ccb);
}
else
{
SDP_TRACE_WARNING ("SDP - Originate failed");
sdpu_release_ccb (p_ccb);
return (NULL);
}
}

其实也很简单,主要就是先分配相应的l2cap的channel,为了下面的SDP流程做好准备。

从hci 的log中 可以看到,l2cap channel 建立起来之后,还会进行对于l2cap 传输参数的配置,log 截图如下:

要分析这个过程,我们需要先 看一下sdp的 注册到l2cap中的情况,实现是在sdp_init 中,这是在整个bluedroid 初始化的时候就会执行,下面我们分析一下整个函数:

/*******************************************************************************
**
** Function sdp_init
**
** Description This function initializes the SDP unit.
**
** Returns void
**
*******************************************************************************/
void sdp_init (void)
{
/* Clears all structures and local SDP database (if Server is enabled) */
memset (&sdp_cb, , sizeof (tSDP_CB)); /* Initialize the L2CAP configuration. We only care about MTU and flush */
sdp_cb.l2cap_my_cfg.mtu_present = TRUE;
sdp_cb.l2cap_my_cfg.mtu = SDP_MTU_SIZE;//
sdp_cb.l2cap_my_cfg.flush_to_present = TRUE;
sdp_cb.l2cap_my_cfg.flush_to = SDP_FLUSH_TO; sdp_cb.max_attr_list_size = SDP_MTU_SIZE - ;
sdp_cb.max_recs_per_search = SDP_MAX_DISC_SERVER_RECS;
...
#if SDP_CLIENT_ENABLED == TRUE
/* Register with Security Manager for the specific security level */
if (!BTM_SetSecurityLevel (TRUE, SDP_SERVICE_NAME, BTM_SEC_SERVICE_SDP_SERVER,
SDP_SECURITY_LEVEL, SDP_PSM, , ))
{
SDP_TRACE_ERROR ("Security Registration for Client failed");
return;
}
#endif #if defined(SDP_INITIAL_TRACE_LEVEL)
sdp_cb.trace_level = SDP_INITIAL_TRACE_LEVEL;//设置trace level
#else
sdp_cb.trace_level = BT_TRACE_LEVEL_NONE; /* No traces */
#endif sdp_cb.reg_info.pL2CA_ConnectInd_Cb = sdp_connect_ind;//接受到对端发过来的连接消息
sdp_cb.reg_info.pL2CA_ConnectCfm_Cb = sdp_connect_cfm;//接受到对端的连接反馈
sdp_cb.reg_info.pL2CA_ConnectPnd_Cb = NULL;
sdp_cb.reg_info.pL2CA_ConfigInd_Cb = sdp_config_ind;//收到对端的配置信息
sdp_cb.reg_info.pL2CA_ConfigCfm_Cb = sdp_config_cfm;//收到对端的配置反馈
sdp_cb.reg_info.pL2CA_DisconnectInd_Cb = sdp_disconnect_ind;
sdp_cb.reg_info.pL2CA_DisconnectCfm_Cb = sdp_disconnect_cfm;
sdp_cb.reg_info.pL2CA_QoSViolationInd_Cb = NULL;
sdp_cb.reg_info.pL2CA_DataInd_Cb = sdp_data_ind;
sdp_cb.reg_info.pL2CA_CongestionStatus_Cb = NULL;
sdp_cb.reg_info.pL2CA_TxComplete_Cb = NULL; /* Now, register with L2CAP */
if (!L2CA_Register (SDP_PSM, &sdp_cb.reg_info))//注册到来l2cap
{
SDP_TRACE_ERROR ("SDP Registration failed");
} }

这里的注册和GATT的注册的思路也是一样的。

当对端 回复我们 connect 的消息的时候,执行sdp_connect_cfm,我们看看其具体的实现:

/*******************************************************************************
**
** Function sdp_connect_cfm
**
** Description This function handles the connect confirm events
** from L2CAP. This is the case when we are acting as a
** client and have sent a connect request.
**
** Returns void
**
*******************************************************************************/
static void sdp_connect_cfm (UINT16 l2cap_cid, UINT16 result)
{
tCONN_CB *p_ccb;
tL2CAP_CFG_INFO cfg; /* Find CCB based on CID */
if ((p_ccb = sdpu_find_ccb_by_cid (l2cap_cid)) == NULL)
{
SDP_TRACE_WARNING ("SDP - Rcvd conn cnf for unknown CID 0x%x", l2cap_cid);
return;
} /* If the connection response contains success status, then */
/* Transition to the next state and startup the timer. */
if ((result == L2CAP_CONN_OK) && (p_ccb->con_state == SDP_STATE_CONN_SETUP))
{
p_ccb->con_state = SDP_STATE_CFG_SETUP; cfg = sdp_cb.l2cap_my_cfg; if (cfg.fcr_present)
{
/*打印*/
} if ((!L2CA_ConfigReq (l2cap_cid, &cfg)) && cfg.fcr_present
&& cfg.fcr.mode != L2CAP_FCR_BASIC_MODE)//开始配置
{
/* FCR not desired; try again in basic mode */
cfg.fcr_present = FALSE;
cfg.fcr.mode = L2CAP_FCR_BASIC_MODE;
L2CA_ConfigReq (l2cap_cid, &cfg);
} SDP_TRACE_EVENT ("SDP - got conn cnf, sent cfg req, CID: 0x%x", p_ccb->connection_id);
}
else
{
...
}
sdpu_release_ccb (p_ccb);
}
}

从上面的代码,我们看到其从Code: Connection Request到Code: Configure Request流程的转换,我们继续看sdp_config_cfm的实现,预计这里应该会转到真正做服务搜索的动作。前面我们在分析SDP_ServiceSearchAttributeRequest的时候是没有看见其做搜索的动作,只是在挑选一些参数,然后做l2cap的连接。

/*******************************************************************************
**
** Function sdp_config_cfm
**
** Description This function processes the L2CAP configuration confirmation
** event.
**
** Returns void
**
*******************************************************************************/
static void sdp_config_cfm (UINT16 l2cap_cid, tL2CAP_CFG_INFO *p_cfg)
{
tCONN_CB *p_ccb; /* Find CCB based on CID */
if ((p_ccb = sdpu_find_ccb_by_cid (l2cap_cid)) == NULL)
{
SDP_TRACE_WARNING ("SDP - Rcvd L2CAP cfg ind, unknown CID: 0x%x", l2cap_cid);
return;
} /* For now, always accept configuration from the other side */
if (p_cfg->result == L2CAP_CFG_OK)
{
p_ccb->con_flags |= SDP_FLAGS_MY_CFG_DONE; if (p_ccb->con_flags & SDP_FLAGS_HIS_CFG_DONE)
{
p_ccb->con_state = SDP_STATE_CONNECTED; if (p_ccb->con_flags & SDP_FLAGS_IS_ORIG)
sdp_disc_connected (p_ccb);//这里开始引向搜索
else
/* Start inactivity timer */
btu_start_timer (&p_ccb->timer_entry, BTU_TTYPE_SDP, SDP_INACT_TIMEOUT);//如果我们不是org,那么过timeout 时间会断开
}
}
else
{
/* If peer has rejected FCR and suggested basic then try basic */
if (p_cfg->fcr_present)//如果失败会retry
{
tL2CAP_CFG_INFO cfg = sdp_cb.l2cap_my_cfg;
cfg.fcr_present = FALSE;
L2CA_ConfigReq (l2cap_cid, &cfg); /* Remain in configure state */
return;
} #if SDP_CLIENT_ENABLED == TRUE
sdp_disconnect(p_ccb, SDP_CFG_FAILED);
#endif
}
}

上面过程的核心的就是sdp_disc_connected ,

/*******************************************************************************
**
** Function sdp_disc_connected
**
** Description This function is called when an SDP discovery attempt is
** connected.
**
** Returns void
**
*******************************************************************************/
void sdp_disc_connected (tCONN_CB *p_ccb)
{
if (p_ccb->is_attr_search)
{
p_ccb->disc_state = SDP_DISC_WAIT_SEARCH_ATTR; process_service_search_attr_rsp (p_ccb, NULL);
}
else
{
/* First step is to get a list of the handles from the server. */
/* We are not searching for a specific attribute, so we will */
/* first search for the service, then get all attributes of it */ p_ccb->num_handles = ;
sdp_snd_service_search_req(p_ccb, , NULL);//开始搜索
} }

也就是 红框中的过程:

这里发现,当数据传输完毕,还会把这条l2cap channel 解除掉,我们继续分析源码来寻找该过程的实现:

可以猜想,该过程肯定是在 sdp response中引向 channel 断开的流程的。我们先看看sdp response的执行函数:

根据sdp_init的注册信息:

  sdp_cb.reg_info.pL2CA_DataInd_Cb = sdp_data_ind;

我们一下子就能想到 执行的是 sdp_data_ind,在sdp_init 中,我们没有发现其实是处理client 端的信息还是server端的信息,这种情况说明肯定会在这个函数中细分,根据不同的情况,路由到不同的函数去处理:

/*******************************************************************************
**
** Function sdp_data_ind
**
** Description This function is called when data is received from L2CAP.
** if we are the originator of the connection, we are the SDP
** client, and the received message is queued up for the client.
**
** If we are the destination of the connection, we are the SDP
** server, so the message is passed to the server processing
** function.
**
** Returns void
**
*******************************************************************************/
static void sdp_data_ind (UINT16 l2cap_cid, BT_HDR *p_msg)
{
tCONN_CB *p_ccb; /* Find CCB based on CID */
if ((p_ccb = sdpu_find_ccb_by_cid (l2cap_cid)) != NULL)
{
if (p_ccb->con_state == SDP_STATE_CONNECTED)
{
if (p_ccb->con_flags & SDP_FLAGS_IS_ORIG)
sdp_disc_server_rsp (p_ccb, p_msg);//本地是client
else
sdp_server_handle_client_req (p_ccb, p_msg);
}
else
{
SDP_TRACE_WARNING ("SDP - Ignored L2CAP data while in state: %d, CID: 0x%x",
p_ccb->con_state, l2cap_cid);
}
}
else
{
SDP_TRACE_WARNING ("SDP - Rcvd L2CAP data, unknown CID: 0x%x", l2cap_cid);
} GKI_freebuf (p_msg);
}

我们继续看看sdp_disc_server_rsp的实现:这里面主要做的就是 对于服务器发过来的信息的保存和处理,具体就不分析了:

/*******************************************************************************
**
** Function process_service_search_attr_rsp
**
** Description This function is called when there is a search attribute
** response from the server.
**
** Returns void
**
*******************************************************************************/
static void process_service_search_attr_rsp (tCONN_CB *p_ccb, UINT8 *p_reply)
{
UINT8 *p, *p_start, *p_end, *p_param_len;
UINT8 type;
UINT32 seq_len;
UINT16 param_len, lists_byte_count = ;
BOOLEAN cont_request_needed = FALSE;
...
/* Since we got everything we need, disconnect the call */
sdp_disconnect (p_ccb, SDP_SUCCESS);//全部处理完成,断开这个ccb连接
}

分析到这里,其实我们只分析了搜索service的一个回合,sdp 的搜索往往有很多回合的,下面我们分析一下,本次搜索介绍是如何触发下一个回合的搜索的。这里我们可以自己先思考一下,第一次搜索的时候,我们搜索的reserved 的服务,并且在bta_dm_search_cb中把已经搜索过的标志清掉了:bta_dm_search_cb.services_to_search &= ~BTA_RES_SERVICE_MASK

那我们猜想,下一次搜索前肯定还是会判断bta_dm_search_cb.services_to_search来check 服务搜索是否完成,

下面我们继续分析,看看对端给本端发了sdp disconnect 的response 之后,本端是如何处理的:

/*******************************************************************************
**
** Function sdp_disconnect_cfm
**
** Description This function handles a disconnect confirm event from L2CAP.
**
** Returns void
**
*******************************************************************************/
static void sdp_disconnect_cfm (UINT16 l2cap_cid, UINT16 result)
{
tCONN_CB *p_ccb;
UNUSED(result); /* Find CCB based on CID */
if ((p_ccb = sdpu_find_ccb_by_cid (l2cap_cid)) == NULL)
{
SDP_TRACE_WARNING ("SDP - Rcvd L2CAP disc cfm, unknown CID: 0x%x", l2cap_cid);
return;
} /* Tell the user if he has a callback */
if (p_ccb->p_cb)
(*p_ccb->p_cb) (p_ccb->disconnect_reason);//调用callback=bta_dm_sdp_callback
else if (p_ccb->p_cb2)
(*p_ccb->p_cb2) (p_ccb->disconnect_reason, p_ccb->user_data); sdpu_release_ccb (p_ccb);//这里才真正去释放ccb
}

我们继续看bta_dm_sdp_callback回调:

/*******************************************************************************
**
** Function bta_dm_sdp_callback
**
** Description Callback from sdp with discovery status
**
** Returns void
**
*******************************************************************************/
static void bta_dm_sdp_callback (UINT16 sdp_status)
{ tBTA_DM_SDP_RESULT * p_msg; if ((p_msg = (tBTA_DM_SDP_RESULT *) GKI_getbuf(sizeof(tBTA_DM_SDP_RESULT))) != NULL)
{
p_msg->hdr.event = BTA_DM_SDP_RESULT_EVT;//0x205
p_msg->sdp_result = sdp_status;
bta_sys_sendmsg(p_msg); }
}

我们看看这个时间的处理:

BOOLEAN bta_dm_search_sm_execute(BT_HDR *p_msg)
{
tBTA_DM_ST_TBL state_table;
UINT8 action;
int i;
/* look up the state table for the current state */
state_table = bta_dm_search_st_tbl[bta_dm_search_cb.state]; bta_dm_search_cb.state = state_table[p_msg->event & 0x00ff][BTA_DM_SEARCH_NEXT_STATE]; /* execute action functions */
for (i = ; i < BTA_DM_SEARCH_ACTIONS; i++)
{
if ((action = state_table[p_msg->event & 0x00ff][i]) != BTA_DM_SEARCH_IGNORE)
{
(*bta_dm_search_action[action])( (tBTA_DM_MSG*) p_msg);
}
}
return TRUE;
}

看看其状态表:

/* SDP_RESULT_EVT */        {BTA_DM_SDP_RESULT,                BTA_DM_SEARCH_IGNORE,          BTA_DM_DISCOVER_ACTIVE},

下一个状态还是BTA_DM_DISCOVER_ACTIVE,执行的行为是BTA_DM_SDP_RESULT:我们看看其实现,因为代码量非常的大,这里删除了一些无关的代码:

    void bta_dm_sdp_result (tBTA_DM_MSG *p_data)
{ tSDP_DISC_REC *p_sdp_rec = NULL;
tBTA_DM_MSG *p_msg;
BOOLEAN scn_found = FALSE;
UINT16 service = 0xFFFF;
tSDP_PROTOCOL_ELEM pe; UINT32 num_uuids = ;
UINT8 uuid_list[][MAX_UUID_SIZE]; // assuming a max of 32 services if((p_data->sdp_event.sdp_result == SDP_SUCCESS)
|| (p_data->sdp_event.sdp_result == SDP_NO_RECS_MATCH)
|| (p_data->sdp_event.sdp_result == SDP_DB_FULL))//处理结果
{
do
{
p_sdp_rec = NULL;
if( bta_dm_search_cb.service_index == BTA_USER_SERVICE_ID+)
{
...
}
else
{
service = bta_service_id_to_uuid_lkup_tbl[bta_dm_search_cb.service_index-];//转换成相应的UUID
p_sdp_rec = SDP_FindServiceInDb(bta_dm_search_cb.p_sdp_db, service, p_sdp_rec);//讲搜索到的service 都保存在p_sdp_rec中
} {
/* SDP_DB_FULL means some records with the
required attributes were received */
if (((p_data->sdp_event.sdp_result == SDP_DB_FULL) &&
bta_dm_search_cb.services != BTA_ALL_SERVICE_MASK) ||
(p_sdp_rec != NULL))
{
if (service != UUID_SERVCLASS_PNP_INFORMATION)//说明是基于l2cap的搜索
{
UINT16 tmp_svc = 0xFFFF;
bta_dm_search_cb.services_found |= (tBTA_SERVICE_MASK)(BTA_SERVICE_ID_TO_SERVICE_MASK(bta_dm_search_cb.service_index-));
tmp_svc = bta_service_id_to_uuid_lkup_tbl[bta_dm_search_cb.service_index-];
/* Add to the list of UUIDs */
sdpu_uuid16_to_uuid128(tmp_svc, uuid_list[num_uuids]);//保存在uuid_list 里面
num_uuids++;
}
}
} if(bta_dm_search_cb.services == BTA_ALL_SERVICE_MASK &&
bta_dm_search_cb.services_to_search == )
{
bta_dm_search_cb.service_index++;
}
else /* regular one service per search or PNP search */
break;
}
while(bta_dm_search_cb.service_index <= BTA_MAX_SERVICE_ID); // GKI_freebuf(bta_dm_search_cb.p_sdp_db);
// bta_dm_search_cb.p_sdp_db = NULL; /* Collect the 128-bit services here and put them into the list */
if(bta_dm_search_cb.services == BTA_ALL_SERVICE_MASK)
{
p_sdp_rec = NULL;
do
{
tBT_UUID temp_uuid;
/* find a service record, report it */
p_sdp_rec = SDP_FindServiceInDb_128bit(bta_dm_search_cb.p_sdp_db, p_sdp_rec);
if (p_sdp_rec)
{
if (SDP_FindServiceUUIDInRec_128bit(p_sdp_rec, &temp_uuid))
{
memcpy(uuid_list[num_uuids], temp_uuid.uu.uuid128, MAX_UUID_SIZE);
num_uuids++;
}
}
} while (p_sdp_rec);
}
/* if there are more services to search for */
if(bta_dm_search_cb.services_to_search)//如果只是搜索了reserved的服务,那么继续搜索
{
/* Free up the p_sdp_db before checking the next one */
bta_dm_free_sdp_db(NULL);
bta_dm_find_services(bta_dm_search_cb.peer_bdaddr);
}
else//否则发送BTA_DM_DISCOVERY_RESULT_EVT 消息
{
/* callbacks */
/* start next bd_addr if necessary */
BTM_SecDeleteRmtNameNotifyCallback(&bta_dm_service_search_remname_cback); if ((p_msg = (tBTA_DM_MSG *) GKI_getbuf(sizeof(tBTA_DM_MSG))) != NULL)
{
p_msg->hdr.event = BTA_DM_DISCOVERY_RESULT_EVT;
p_msg->disc_result.result.disc_res.result = BTA_SUCCESS;
p_msg->disc_result.result.disc_res.p_raw_data = NULL;
p_msg->disc_result.result.disc_res.raw_data_size = ;
p_msg->disc_result.result.disc_res.num_uuids = num_uuids;
p_msg->disc_result.result.disc_res.p_uuid_list = NULL;
if (num_uuids > ) {
p_msg->disc_result.result.disc_res.p_uuid_list = (UINT8*)GKI_getbuf(num_uuids*MAX_UUID_SIZE);
if (p_msg->disc_result.result.disc_res.p_uuid_list) {
memcpy(p_msg->disc_result.result.disc_res.p_uuid_list, uuid_list,
num_uuids*MAX_UUID_SIZE);
}
...
}
//copy the raw_data to the discovery result structure
// if ( bta_dm_search_cb.p_sdp_db != NULL && bta_dm_search_cb.p_sdp_db->raw_used != &&
bta_dm_search_cb.p_sdp_db->raw_data != NULL) { p_msg->disc_result.result.disc_res.p_raw_data = GKI_getbuf(bta_dm_search_cb.p_sdp_db->raw_used);
if ( NULL != p_msg->disc_result.result.disc_res.p_raw_data ) {
memcpy( p_msg->disc_result.result.disc_res.p_raw_data,
bta_dm_search_cb.p_sdp_db->raw_data,
bta_dm_search_cb.p_sdp_db->raw_used ); p_msg->disc_result.result.disc_res.raw_data_size =
bta_dm_search_cb.p_sdp_db->raw_used; }
...
bta_dm_search_cb.p_sdp_db->raw_data = NULL; //no need to free this - it is a global assigned.
bta_dm_search_cb.p_sdp_db->raw_used = ;
bta_dm_search_cb.p_sdp_db->raw_size = ;
} /* Done with p_sdp_db. Free it */
bta_dm_free_sdp_db(NULL);
p_msg->disc_result.result.disc_res.services = bta_dm_search_cb.services_found;
...
bdcpy (p_msg->disc_result.result.disc_res.bd_addr, bta_dm_search_cb.peer_bdaddr);
BCM_STRNCPY_S((char*)p_msg->disc_result.result.disc_res.bd_name, sizeof(BD_NAME),
bta_dm_get_remname(), (BD_NAME_LEN-));
/* make sure the string is null terminated */
p_msg->disc_result.result.disc_res.bd_name[BD_NAME_LEN-] = ;
bta_sys_sendmsg(p_msg);
}
}
}
}

上面的代码 实现看起来 比较乱,其实逻辑其实也不复杂。主要做的事情如下:

  1. 判断是否是UUID_SERVCLASS_PNP_INFORMATION,如果是,继续进行搜索
  2. 否则 说明已经完成了 基于L2CAP的service的搜索,那么进行数据保存,解析,以及上报,上报调用的时间是BTA_DM_DISCOVERY_RESULT_EVT

我们下面继续分析BTA_DM_DISCOVERY_RESULT_EVT 的流程:

BTA got event 0x207
/* DISCV_RES_EVT */         {BTA_DM_DISC_RESULT,               BTA_DM_SEARCH_IGNORE,          BTA_DM_DISCOVER_ACTIVE},

发现下一个状态依然是BTA_DM_DISCOVER_ACTIVE,执行的动作是BTA_DM_DISC_RESULT,我们继续看这个函数的实现:

/*******************************************************************************
**
** Function bta_dm_disc_result
**
** Description Service discovery result when discovering services on a device
**
** Returns void
**
*******************************************************************************/
void bta_dm_disc_result (tBTA_DM_MSG *p_data)
{
APPL_TRACE_EVENT("%s", __func__);
bta_dm_search_cb.p_search_cback(BTA_DM_DISC_RES_EVT, &p_data->disc_result.result);//调用回调上报BTA_DM_DISC_RES_EVT tBTA_DM_MSG *p_msg = (tBTA_DM_MSG *) GKI_getbuf(sizeof(tBTA_DM_MSG)); /* send a message to change state */
if (p_msg != NULL)
{
p_msg->hdr.event = BTA_DM_SEARCH_CMPL_EVT;//再次发送event
p_msg->hdr.layer_specific = BTA_DM_API_DISCOVER_EVT;
bta_sys_sendmsg(p_msg);
}
}

上面的代码也是分为两个部分:

  1. bta_dm_search_cb.p_search_cback(BTA_DM_DISC_RES_EVT, &p_data->disc_result.result); 进行上报数据和状态到 device manager
  2. 继续进行状态机的轮转,因为此刻的状态依然是BTA_DM_DISCOVER_ACTIVE,按照预期,最后应该是idle的状态。

我们先看:

bta_dm_search_cb.p_search_cback(BTA_DM_DISC_RES_EVT, &p_data->disc_result.result);

这个回调,我们也很熟悉,每次搜索都会有这个回调,执行的函数是bte_dm_search_services_evt,其实这个函数也是另外的函数的封装,

/*******************************************************************************
**
** Function bte_dm_search_services_evt
**
** Description Switches context from BTE to BTIF for DM search services
** event
**
** Returns void
**
*******************************************************************************/
static void bte_dm_search_services_evt(tBTA_DM_SEARCH_EVT event, tBTA_DM_SEARCH *p_data)
{
UINT16 param_len = ;
if (p_data)
param_len += sizeof(tBTA_DM_SEARCH);
switch (event)
{
case BTA_DM_DISC_RES_EVT:
{
if ((p_data->disc_res.result == BTA_SUCCESS) && (p_data->disc_res.num_uuids > )) {
param_len += (p_data->disc_res.num_uuids * MAX_UUID_SIZE);
}
} break;
}
/* TODO: The only other member that needs a deep copy is the p_raw_data. But not sure
* if raw_data is needed. */
btif_transfer_context(btif_dm_search_services_evt, event, (char*)p_data, param_len,
(param_len > sizeof(tBTA_DM_SEARCH)) ? search_services_copy_cb : NULL);
}

我们继续看btif_dm_search_services_evt的实现,

/*******************************************************************************
**
** Function btif_dm_search_services_evt
**
** Description Executes search services event in btif context
**
** Returns void
**
*******************************************************************************/
static void btif_dm_search_services_evt(UINT16 event, char *p_param)
{
tBTA_DM_SEARCH *p_data = (tBTA_DM_SEARCH*)p_param; switch (event)
{
case BTA_DM_DISC_RES_EVT:
{
bt_property_t prop;
uint32_t i = ;
bt_bdaddr_t bd_addr;
bt_status_t ret; bdcpy(bd_addr.address, p_data->disc_res.bd_addr); if ((p_data->disc_res.result != BTA_SUCCESS) &&
(pairing_cb.state == BT_BOND_STATE_BONDING ) &&
(pairing_cb.sdp_attempts < BTIF_DM_MAX_SDP_ATTEMPTS_AFTER_PAIRING))//sdp的最大的retry的次数是BTIF_DM_MAX_SDP_ATTEMPTS_AFTER_PAIRING = 2
{
BTIF_TRACE_WARNING("%s:SDP failed after bonding re-attempting", __FUNCTION__);
pairing_cb.sdp_attempts++;
btif_dm_get_remote_services(&bd_addr);//失败之后,重新进行服务搜索
return;
}
prop.type = BT_PROPERTY_UUIDS;
prop.len = ;
if ((p_data->disc_res.result == BTA_SUCCESS) && (p_data->disc_res.num_uuids > ))
{
prop.val = p_data->disc_res.p_uuid_list;
prop.len = p_data->disc_res.num_uuids * MAX_UUID_SIZE;
for (i=; i < p_data->disc_res.num_uuids; i++)
{
char temp[];
uuid_to_string_legacy((bt_uuid_t*)(p_data->disc_res.p_uuid_list + (i*MAX_UUID_SIZE)), temp);
LOG_INFO("%s index:%d uuid:%s", __func__, i, temp);
}
} /* onUuidChanged requires getBondedDevices to be populated.
** bond_state_changed needs to be sent prior to remote_device_property
*/
if ((pairing_cb.state == BT_BOND_STATE_BONDING) &&
((bdcmp(p_data->disc_res.bd_addr, pairing_cb.bd_addr) == ) ||
(bdcmp(p_data->disc_res.bd_addr, pairing_cb.static_bdaddr.address) == )) &&
pairing_cb.sdp_attempts > )
{
BTIF_TRACE_DEBUG("%s Remote Service SDP done. Call bond_state_changed_cb BONDED",
__FUNCTION__);
pairing_cb.sdp_attempts = ;//复原次变量,sdp流程完成 // If bonding occured due to cross-key pairing, send bonding callback
// for static address now
if (bdcmp(p_data->disc_res.bd_addr, pairing_cb.static_bdaddr.address) == )
bond_state_changed(BT_STATUS_SUCCESS, &bd_addr, BT_BOND_STATE_BONDING); bond_state_changed(BT_STATUS_SUCCESS, &bd_addr, BT_BOND_STATE_BONDED);//sdp完成之后 才上传绑定完成的状态
} if (p_data->disc_res.num_uuids != )
{
/* Also write this to the NVRAM */
ret = btif_storage_set_remote_device_property(&bd_addr, &prop);//保存到文件
/* Send the event to the BTIF */
HAL_CBACK(bt_hal_cbacks, remote_device_properties_cb,
BT_STATUS_SUCCESS, &bd_addr, , &prop);//调用remote_device_properties_cb 汇报到framework
}
}

上面的流程,非常简单,就是上报绑定完成的状态,以及汇报service discovery的结果,其实分析到这里,我们只是分析一个BTA_DM_DISC_RES_EVT,很容易想到,后续肯定还会分析discovery 完成事件:BTA_DM_DISC_CMPL_EVT

,下面我们继续分析第二点:

继续进行状态机的轮转,发送BTA_DM_SEARCH_CMPL_EVT

直接看状态表:

/* SEARCH_CMPL_EVT */       {BTA_DM_SEARCH_CMPL,               BTA_DM_SEARCH_IGNORE,          BTA_DM_SEARCH_IDLE},

我们终于看到了 状态最后轮转到idle 状态了:BTA_DM_SEARCH_IDLE,执行的action是BTA_DM_SEARCH_CMPL,执行的函数是:bta_dm_search_cmpl

/*******************************************************************************
**
** Function bta_dm_search_cmpl
**
** Description Sends event to application
**
** Returns void
**
*******************************************************************************/
void bta_dm_search_cmpl (tBTA_DM_MSG *p_data)
{
APPL_TRACE_EVENT("%s", __func__); if (p_data->hdr.layer_specific == BTA_DM_API_DI_DISCOVER_EVT)
bta_dm_di_disc_cmpl(p_data);
else
bta_dm_search_cb.p_search_cback(BTA_DM_DISC_CMPL_EVT, NULL);//还是调用这个函数,上报BTA_DM_DISC_CMPL_EVT
}

而对于BTA_DM_DISC_CMPL_EVT 的处理,btif_dm_search_services_evt函数并没有去实现,不过这里也不需要做什么,搜索的数据和状态都已经上报给framework了,

那到这里,sdp 的代码流程就分析完了。


SDP服务搜索流程源码分析的更多相关文章

  1. [Android]从Launcher开始启动App流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5017056.html 从Launcher开始启动App流程源码 ...

  2. [Android]Android系统启动流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5013863.html Android系统启动流程源码分析 首先 ...

  3. Spring加载流程源码分析03【refresh】

      前面两篇文章分析了super(this)和setConfigLocations(configLocations)的源代码,本文来分析下refresh的源码, Spring加载流程源码分析01[su ...

  4. Dubbo消费方服务调用过程源码分析

    参考:dubbo消费方服务调用过程源码分析dubbo基于spring的构建分析Dubbo概述--调用过程dubbo 请求调用过程分析dubbo集群容错机制代码分析1dubbo集群容错策略的代码分析2d ...

  5. Android笔记--View绘制流程源码分析(二)

    Android笔记--View绘制流程源码分析二 通过上一篇View绘制流程源码分析一可以知晓整个绘制流程之前,在activity启动过程中: Window的建立(activit.attach生成), ...

  6. Android笔记--View绘制流程源码分析(一)

    Android笔记--View绘制流程源码分析 View绘制之前框架流程分析 View绘制的分析始终是离不开Activity及其内部的Window的.在Activity的源码启动流程中,一并包含 着A ...

  7. Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)

    上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...

  8. Spark(四十九):Spark On YARN启动流程源码分析(一)

    引导: 该篇章主要讲解执行spark-submit.sh提交到将任务提交给Yarn阶段代码分析. spark-submit的入口函数 一般提交一个spark作业的方式采用spark-submit来提交 ...

  9. spring boot 加载web容器tomcat流程源码分析

    spring boot 加载web容器tomcat流程源码分析 我本地的springboot版本是2.5.1,后面的分析都是基于这个版本 <parent> <groupId>o ...

随机推荐

  1. WebView

    WebView可以使得网页轻松的内嵌到app里,还可以直接跟js相互调用. webview有两个方法:setWebChromeClient 和 setWebClient setWebClient:主要 ...

  2. unity3d插件Daikon Forge GUI 中文教程7-高级控件slider的使用

    3.6.slider滑块              我们说说前面这个图片.对象 Appearance: Atlas 图集: Track 滑块的轨道图片 Back color滑块的轨道图片的主颜色. O ...

  3. 使用prototype 对象定义类成员

    使用prototype 对象定义类成员上一节介绍了类的实现机制以及构造函数的实现,现在介绍另一种为类添加成员的机制:prototype 对象.当new 一个function 时,该对象的成员将自动赋给 ...

  4. OSPF LSA的详解

    LSA类型的配置与查看 1基本配置 R1(config)#NO IP DO LO R1(config)#NO ENAble PAssword R1(config)#LINe COnsole 0 R1( ...

  5. poj 3440 Coin Toss 概率问题

    这题主要是推导数学公式!!! 将概率问题转化为圆心所在的面积! 代码如下: #include<iostream> #include<stdio.h> #include<a ...

  6. Maven学习一:用Maven创建Java Project

    转自:http://blog.csdn.net/lfsfxy9/article/details/9399093  Maven环境配置只是入门的基础,现在要通过Maven基本命令生成一个Java Pro ...

  7. Fedora22没有i18n文件

    今天想修改Fedora22的系统语言,发现/etc/sysconfig/没有i18n 解决方法: dnf install system-config-language.noarch -y system ...

  8. GreenDao 使用二

    1.创建一个实体类 1 Entity note = schema.addEntity("Note"); 默认表名就是类名,也可以自定义表名 1.dao.setTableName(& ...

  9. firefox插件开发及源码下载

    在个别情况下,由于数据量巨大,造成显示性能的明显下降,此时使用c++开发firefox插件,可以提高用户使用体验. test.html: 在插件中,我们导出3个函数给js:AddStr, Pause, ...

  10. IIS 无法显示网页 目前访问网站的用户过多

    最近把一个服务部署到XP系统的IIS上,供其他程序调用,在访问了几个页面后,会出现“无法显示网页 目前访问网站的用户过多”的提示. 网上找了,果然有解决方法: 1.打开IIS,在网站上右键,选择“属性 ...