
相关API简介 在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。 WebRTC通信相关的API非常多,主要完成了如下功能: 信令交换 通信候选地址交换 音视频采集 音视频发送、接收 相关API太多,为避免篇幅过长,文中部分采用了伪代码进行讲解。详细代码参考文章末尾,也可以在笔者的Github上找到,有问题欢迎留言交流。 信令交换 信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket。 发送方伪代码如下: const pc = new RTCPeerConnection(iceConfig); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); sendToPeerViaSignalingServer(SIGNALING_OFFER, offer); // 发送方发送信令消息 接收方伪代码如下: const pc = new RTCPeerConnection(iceConfig); await pc.setRemoteDescription(offer); const answer = await pc.createAnswer(); await pc.setLocalDescription(answer); sendToPeerViaSignalingServer(SIGNALING_ANSWER, answer); // 接收方发送信令消息 候选地址交换服务 当本地设置了会话描述信息,并添加了媒体流的情况下,ICE框架就会开始收集候选地址。两边收集到候选地址后,需要交换候选地址,并从中知道合适的候选地址对。 候选地址的交换,同样采用前面提到的信令服务,伪代码如下: // 设置本地会话描述信息 const localPeer = new RTCPeerConnection(iceConfig); const offer = await pc.createOffer(); await localPeer.setLocalDescription(offer); // 本地采集音视频 const localVideo = document.getElementById('local-video'); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = mediaStream; // 添加音视频流 mediaStream.getTracks().forEach(track => { localPeer.addTrack(track, mediaStream); }); // 交换候选地址 localPeer.onicecandidate = function(evt) { if (evt.candidate) { sendToPeerViaSignalingServer(SIGNALING_CANDIDATE, evt.candidate); } } 音视频采集 可以使用浏览器提供的getUserMedia接口,采集本地的音视频。 const localVideo = document.getElementById('local-video'); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = mediaStream; 音视频发送、接收 将采集到的音视频轨道,通过addTrack进行添加,发送给远端。 mediaStream.getTracks().forEach(track => { localPeer.addTrack(track, mediaStream); }); 远端可以通过监听ontrack来监听音视频的到达,并进行播放。 remotePeer.ontrack = function(evt) { const remoteVideo = document.getElementById('remote-video'); remoteVideo.srcObject = evt.streams[0]; } 完整代码 包含两部分:客户端代码、服务端代码。 1、客户端代码 const socket = io.connect('http://localhost:3000'); const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT'; const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT'; const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT'; const SERVER_USER_EVENT = 'SERVER_USER_EVENT'; const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录 const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS'; const SIGNALING_OFFER = 'SIGNALING_OFFER'; const SIGNALING_ANSWER = 'SIGNALING_ANSWER'; const SIGNALING_CANDIDATE = 'SIGNALING_CANDIDATE'; let remoteUser = ''; // 远端用户 let localUser = ''; // 本地登录用户 function log(msg) { console.log(`[client] ${msg}`); } socket.on('connect', function() { log('ws connect.'); }); socket.on('connect_error', function() { log('ws connect_error.'); }); socket.on('error', function(errorMessage) { log('ws error, ' + errorMessage); }); socket.on(SERVER_USER_EVENT, function(msg) { const type = msg.type; const payload = msg.payload; switch(type) { case SERVER_USER_EVENT_UPDATE_USERS: updateUserList(payload); break; } log(`[${SERVER_USER_EVENT}] [${type}], ${JSON.stringify(msg)}`); }); socket.on(SERVER_RTC_EVENT, function(msg) { const {type} = msg; switch(type) { case SIGNALING_OFFER: handleReceiveOffer(msg); break; case SIGNALING_ANSWER: handleReceiveAnswer(msg); break; case SIGNALING_CANDIDATE: handleReceiveCandidate(msg); break; } }); async function handleReceiveOffer(msg) { log(`receive remote description from ${msg.payload.from}`); // 设置远端描述 const remoteDescription = new RTCSessionDescription(msg.payload.sdp); remoteUser = msg.payload.from; createPeerConnection(); await pc.setRemoteDescription(remoteDescription); // TODO 错误处理 // 本地音视频采集 const localVideo = document.getElementById('local-video'); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = mediaStream; mediaStream.getTracks().forEach(track => { pc.addTrack(track, mediaStream); // pc.addTransceiver(track, {streams: [mediaStream]}); // 这个也可以 }); // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃 const answer = await pc.createAnswer(); // TODO 错误处理 await pc.setLocalDescription(answer); sendRTCEvent({ type: SIGNALING_ANSWER, payload: { sdp: answer, from: localUser, target: remoteUser } }); } async function handleReceiveAnswer(msg) { log(`receive remote answer from ${msg.payload.from}`); const remoteDescription = new RTCSessionDescription(msg.payload.sdp); remoteUser = msg.payload.from; await pc.setRemoteDescription(remoteDescription); // TODO 错误处理 } async function handleReceiveCandidate(msg){ log(`receive candidate from ${msg.payload.from}`); await pc.addIceCandidate(msg.payload.candidate); // TODO 错误处理 } /** * 发送用户相关消息给服务器 * @param {Object} msg 格式如 { type: 'xx', payload: {} } */ function sendUserEvent(msg) { socket.emit(CLIENT_USER_EVENT, JSON.stringify(msg)); } /** * 发送RTC相关消息给服务器 * @param {Object} msg 格式如{ type: 'xx', payload: {} } */ function sendRTCEvent(msg) { socket.emit(CLIENT_RTC_EVENT, JSON.stringify(msg)); } let pc = null; /** * 邀请用户加入视频聊天 * 1、本地启动视频采集 * 2、交换信令 */ async function startVideoTalk() { // 开启本地视频 const localVideo = document.getElementById('local-video'); const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); localVideo.srcObject = mediaStream; // 创建 peerConnection createPeerConnection(); // 将媒体流添加到webrtc的音视频收发器 mediaStream.getTracks().forEach(track => { pc.addTrack(track, mediaStream); // pc.addTransceiver(track, {streams: [mediaStream]}); }); // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃 } function createPeerConnection() { const iceConfig = {"iceServers": [ {url: 'stun:stun.ekiga.net'}, {url: 'turn:turnserver.com', username: 'user', credential: 'pass'} ]}; pc = new RTCPeerConnection(iceConfig); pc.onnegotiationneeded = onnegotiationneeded; pc.onicecandidate = onicecandidate; pc.onicegatheringstatechange = onicegatheringstatechange; pc.oniceconnectionstatechange = oniceconnectionstatechange; pc.onsignalingstatechange = onsignalingstatechange; pc.ontrack = ontrack; return pc; } async function onnegotiationneeded() { log(`onnegotiationneeded.`); const offer = await pc.createOffer(); await pc.setLocalDescription(offer); // TODO 错误处理 sendRTCEvent({ type: SIGNALING_OFFER, payload: { from: localUser, target: remoteUser, sdp: pc.localDescription // TODO 直接用offer? } }); } function onicecandidate(evt) { if (evt.candidate) { log(`onicecandidate.`); sendRTCEvent({ type: SIGNALING_CANDIDATE, payload: { from: localUser, target: remoteUser, candidate: evt.candidate } }); } } function onicegatheringstatechange(evt) { log(`onicegatheringstatechange, pc.iceGatheringState is ${pc.iceGatheringState}.`); } function oniceconnectionstatechange(evt) { log(`oniceconnectionstatechange, pc.iceConnectionState is ${pc.iceConnectionState}.`); } function onsignalingstatechange(evt) { log(`onsignalingstatechange, pc.signalingstate is ${pc.signalingstate}.`); } // 调用 pc.addTrack(track, mediaStream),remote peer的 onTrack 会触发两次 // 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用 // 这个行为有点奇怪,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313 let stream; function ontrack(evt) { // if (!stream) { // stream = evt.streams[0]; // } else { // console.log(`${stream === evt.streams[0]}`); // 这里为true // } log(`ontrack.`); const remoteVideo = document.getElementById('remote-video'); remoteVideo.srcObject = evt.streams[0]; } // 点击用户列表 async function handleUserClick(evt) { const target = evt.target; const userName = target.getAttribute('data-name').trim(); if (userName === localUser) { alert('不能跟自己进行视频会话'); return; } log(`online user selected: ${userName}`); remoteUser = userName; await startVideoTalk(remoteUser); } /** * 更新用户列表 * @param {Array} users 用户列表,比如 [{name: '小明', name: '小强'}] */ function updateUserList(users) { const fragment = document.createDocumentFragment(); const userList = document.getElementById('login-users'); userList.innerHTML = ''; users.forEach(user => { const li = document.createElement('li'); li.innerHTML = user.userName; li.setAttribute('data-name', user.userName); li.addEventListener('click', handleUserClick); fragment.appendChild(li); }); userList.appendChild(fragment); } /** * 用户登录 * @param {String} loginName 用户名 */ function login(loginName) { localUser = loginName; sendUserEvent({ type: CLIENT_USER_EVENT_LOGIN, payload: { loginName: loginName } }); } // 处理登录 function handleLogin(evt) { let loginName = document.getElementById('login-name').value.trim(); if (loginName === '') { alert('用户名为空!'); return; } login(loginName); } function init() { document.getElementById('login-btn').addEventListener('click', handleLogin); } init(); 2、服务端代码 // 添加ws服务 const io = require('socket.io')(server); let connectionList = []; const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT'; const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT'; const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT'; const SERVER_USER_EVENT = 'SERVER_USER_EVENT'; const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS'; function getOnlineUser() { return connectionList .filter(item => { return item.userName !== ''; }) .map(item => { return { userName: item.userName }; }); } function setUserName(connection, userName) { connectionList.forEach(item => { if (item.connection.id === connection.id) { item.userName = userName; } }); } function updateUsers(connection) { connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()}); } io.on('connection', function (connection) { connectionList.push({ connection: connection, userName: '' }); // 连接上的用户,推送在线用户列表 // connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()}); updateUsers(connection); connection.on(CLIENT_USER_EVENT, function(jsonString) { const msg = JSON.parse(jsonString); const {type, payload} = msg; if (type === CLIENT_USER_EVENT_LOGIN) { setUserName(connection, payload.loginName); connectionList.forEach(item => { // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()}); updateUsers(item.connection); }); } }); connection.on(CLIENT_RTC_EVENT, function(jsonString) { const msg = JSON.parse(jsonString); const {payload} = msg; const target = payload.target; const targetConn = connectionList.find(item => { return item.userName === target; }); if (targetConn) { targetConn.connection.emit(SERVER_RTC_EVENT, msg); } }); connection.on('disconnect', function () { connectionList = connectionList.filter(item => { return item.connection.id !== connection.id; }); connectionList.forEach(item => { // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()}); updateUsers(item.connection); }); }); }); 写在后面 WebRTC的API非常多,因为WebRTC本身就比较复杂,随着时间的推移,WebRTC的某些API(包括某些协议细节)也在改动或被废弃,这其中也有向后兼容带来的复杂性,比如本地视频采集后加入传输流,可以采用 addStream 或 addTrack 或 addTransceiver,再比如会话描述版本从plan-b迁移到unified-plan。 建议亲自动手撸一遍代码,加深了解。 相关链接 2019.08.02-video-talk-using-webrtc https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection onremotestream called twice for each remote stream
一、内容概述 在MySQL的使用过程中,了解字符集、字符序的概念,以及不同设置对数据存储、比较的影响非常重要。不少同学在日常工作中遇到的“乱码”问题,很有可能就是因为对字符集与字符序的理解不到位、设置错误造成的。 本文由浅入深,分别介绍了如下内容: 字符集、字符序的基本概念及联系 MySQL支持的字符集、字符序设置级,各设置级别之间的联系 server、database、table、column级字符集、字符序的查看及设置 应该何时设置字符集、字符序 二、字符集、字符序的概念与联系 在数据的存储上,MySQL提供了不同的字符集支持。而在数据的对比操作上,则提供了不同的字符序支持。 MySQL提供了不同级别的设置,包括server级、database级、table级、column级,可以提供非常精准的设置。 什么是字符集、字符序?简单的来说: 字符集(character set):定义了字符以及字符的编码。 字符序(collation):定义了字符的比较规则。 举个例子: 有四个字符:A、B、a、b,这四个字符的编码分别是A = 0, B = 1, a = 2, b = 3。这里的字符 + 编码就构成了字符集(character set)。 如果我们想比较两个字符的大小呢?比如A、B,或者a、b,最直观的比较方式是采用它们的编码,比如因为0 < 1,所以 A < B。 另外,对于A、a,虽然它们编码不同,但我们觉得大小写字符应该是相等的,也就是说 A == a。 这上面定义了两条比较规则,这些比较规则的集合就是collation。 同样是大写字符、小写字符,则比较他们的编码大小; 如果两个字符为大小写关系,则它们相等。 三、MySQL支持的字符集、字符序 MySQL支持多种字符集 与 字符序。 一个字符集对应至少一种字符序(一般是1对多)。 两个不同的字符集不能有相同的字符序。 每个字符集都有默认的字符序。 上面说的比较抽象,我们看下后面几个小节就知道怎么回事了。 1、查看支持的字符集 可以通过以下方式查看MYSQL支持的字符集。 方式一: mysql> SHOW CHARACTER SET; +----------+-----------------------------+---------------------+--------+ | Charset | Description | Default collation | Maxlen | +----------+-----------------------------+---------------------+--------+ | big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 | | dec8 | DEC West European | dec8_swedish_ci | 1 | ...省略 方式二: mysql> use information_schema; mysql> select * from CHARACTER_SETS; +--------------------+----------------------+-----------------------------+--------+ | CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION | MAXLEN | +--------------------+----------------------+-----------------------------+--------+ | big5 | big5_chinese_ci | Big5 Traditional Chinese | 2 | | dec8 | dec8_swedish_ci | DEC West European | 1 | ...省略 当使用SHOW CHARACTER SET查看时,也可以加上WHERE或LIKE限定条件。 例子一:使用WHERE限定条件。 mysql> SHOW CHARACTER SET WHERE Charset="utf8"; +---------+---------------+-------------------+--------+ | Charset | Description | Default collation | Maxlen | +---------+---------------+-------------------+--------+ | utf8 | UTF-8 Unicode | utf8_general_ci | 3 | +---------+---------------+-------------------+--------+ 1 row in set (0.00 sec) 例子二:使用LIKE限定条件。 mysql> SHOW CHARACTER SET LIKE "utf8%"; +---------+---------------+--------------------+--------+ | Charset | Description | Default collation | Maxlen | +---------+---------------+--------------------+--------+ | utf8 | UTF-8 Unicode | utf8_general_ci | 3 | | utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 | +---------+---------------+--------------------+--------+ 2 rows in set (0.00 sec) 2、查看支持的字符序 类似的,可以通过如下方式查看MYSQL支持的字符序。 方式一:通过SHOW COLLATION进行查看。 可以看到,utf8字符集有超过10种字符序。通过Default的值是否为Yes,判断是否默认的字符序。 mysql> SHOW COLLATION WHERE Charset = 'utf8'; +--------------------------+---------+-----+---------+----------+---------+ | Collation | Charset | Id | Default | Compiled | Sortlen | +--------------------------+---------+-----+---------+----------+---------+ | utf8_general_ci | utf8 | 33 | Yes | Yes | 1 | | utf8_bin | utf8 | 83 | | Yes | 1 | ...略 方式二:查询information_schema.COLLATIONS。 mysql> USE information_schema; mysql> SELECT * FROM COLLATIONS WHERE CHARACTER_SET_NAME="utf8"; +--------------------------+--------------------+-----+------------+-------------+---------+ | COLLATION_NAME | CHARACTER_SET_NAME | ID | IS_DEFAULT | IS_COMPILED | SORTLEN | +--------------------------+--------------------+-----+------------+-------------+---------+ | utf8_general_ci | utf8 | 33 | Yes | Yes | 1 | | utf8_bin | utf8 | 83 | | Yes | 1 | | utf8_unicode_ci | utf8 | 192 | | Yes | 8 | 3、字符序的命名规范 字符序的命名,以其对应的字符集作为前缀,如下所示。比如字符序utf8_general_ci,标明它是字符集utf8的字符序。 更多规则可以参考 官方文档。 MariaDB [information_schema]> SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM COLLATIONS WHERE CHARACTER_SET_NAME="utf8" limit 2; +--------------------+-----------------+ | CHARACTER_SET_NAME | COLLATION_NAME | +--------------------+-----------------+ | utf8 | utf8_general_ci | | utf8 | utf8_bin | +--------------------+-----------------+ 2 rows in set (0.00 sec) 四、server的字符集、字符序 用途:当你创建数据库,且没有指定字符集、字符序时,server字符集、server字符序就会作为该数据库的默认字符集、排序规则。 如何指定:MySQL服务启动时,可通过命令行参数指定。也可以通过配置文件的变量指定。 server默认字符集、字符序:在MySQL编译的时候,通过编译参数指定。 character_set_server、collation_server分别对应server字符集、server字符序。 1、查看server字符集、字符序 分别对应character_set_server、collation_server两个系统变量。 mysql> SHOW VARIABLES LIKE "character_set_server"; mysql> SHOW VARIABLES LIKE "collation_server"; 2、启动服务时指定 可以在MySQL服务启动时,指定server字符集、字符序。如不指定,默认的字符序分别为latin1、latin1_swedish_ci mysqld --character-set-server=latin1 \ --collation-server=latin1_swedish_ci 单独指定server字符集,此时,server字符序为latin1的默认字符序latin1_swedish_ci。 mysqld --character-set-server=latin1 3、配置文件指定 除了在命令行参数里指定,也可以在配置文件里指定,如下所示。 [client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqld] collation-server = utf8_unicode_ci init-connect='SET NAMES utf8' character-set-server = utf8 4、运行时修改 例子:运行时修改(重启后会失效,如果想要重启后保持不变,需要写进配置文件里) mysql> SET character_set_server = utf8 ; 5、编译时指定默认字符集、字符序 character_set_server、collation_server的默认值,可以在MySQL编译时,通过编译选项指定: cmake . -DDEFAULT_CHARSET=latin1 \ -DDEFAULT_COLLATION=latin1_german1_ci 五、database的字符集、字符序 用途:指定数据库级别的字符集、字符序。同一个MySQL服务下的数据库,可以分别指定不同的字符集/字符序。 1、设置数据的字符集/字符序 可以在创建、修改数据库的时候,通过CHARACTER SET、COLLATE指定数据库的字符集、排序规则。 创建数据库: CREATE DATABASE db_name [[DEFAULT] CHARACTER SET charset_name] [[DEFAULT] COLLATE collation_name] 修改数据库: ALTER DATABASE db_name [[DEFAULT] CHARACTER SET charset_name] [[DEFAULT] COLLATE collation_name] 例子:创建数据库test_schema,字符集设置为utf8,此时默认的排序规则为utf8_general_ci。 CREATE DATABASE `test_schema` DEFAULT CHARACTER SET utf8; 2、查看数据库的字符集/字符序 有3种方式可以查看数据库的字符集/字符序。 例子一:查看test_schema的字符集、排序规则。(需要切换默认数据库) mysql> use test_schema; Database changed mysql> SELECT @@character_set_database, @@collation_database; +--------------------------+----------------------+ | @@character_set_database | @@collation_database | +--------------------------+----------------------+ | utf8 | utf8_general_ci | +--------------------------+----------------------+ 1 row in set (0.00 sec) 例子二:也可以通过下面命令查看test_schema的字符集、数据库(不需要切换默认数据库) mysql> SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE schema_name="test_schema"; +-------------+----------------------------+------------------------+ | SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | +-------------+----------------------------+------------------------+ | test_schema | utf8 | utf8_general_ci | +-------------+----------------------------+------------------------+ 1 row in set (0.00 sec) 例子三:也可以通过查看创建数据库的语句,来查看字符集。 mysql> SHOW CREATE DATABASE test_schema; +-------------+----------------------------------------------------------------------+ | Database | Create Database | +-------------+----------------------------------------------------------------------+ | test_schema | CREATE DATABASE `test_schema` /*!40100 DEFAULT CHARACTER SET utf8 */ | +-------------+----------------------------------------------------------------------+ 1 row in set (0.00 sec) 3、database字符集、字符序是怎么确定的 创建数据库时,指定了CHARACTER SET或COLLATE,则以对应的字符集、排序规则为准。 创建数据库时,如果没有指定字符集、排序规则,则以character_set_server、collation_server为准。 六、table的字符集、字符序 创建表、修改表的语法如下,可通过CHARACTER SET、COLLATE设置字符集、字符序。 CREATE TABLE tbl_name (column_list) [[DEFAULT] CHARACTER SET charset_name] [COLLATE collation_name]] ALTER TABLE tbl_name [[DEFAULT] CHARACTER SET charset_name] [COLLATE collation_name] 1、创建table并指定字符集/字符序 例子如下,指定字符集为utf8,字符序则采用默认的。 CREATE TABLE `test_schema`.`test_table` ( `id` INT NOT NULL COMMENT '', PRIMARY KEY (`id`) COMMENT '') DEFAULT CHARACTER SET = utf8; 2、查看table的字符集/字符序 同样,有3种方式可以查看table的字符集/字符序。 方式一:通过SHOW TABLE STATUS查看table状态,注意Collation为utf8_general_ci,对应的字符集为utf8。 MariaDB [blog]> SHOW TABLE STATUS FROM test_schema \G; *************************** 1. row *************************** Name: test_table Engine: InnoDB Version: 10 Row_format: Compact Rows: 0 Avg_row_length: 0 Data_length: 16384 Max_data_length: 0 Index_length: 0 Data_free: 11534336 Auto_increment: NULL Create_time: 2018-01-09 16:10:42 Update_time: NULL Check_time: NULL Collation: utf8_general_ci Checksum: NULL Create_options: Comment: 1 row in set (0.00 sec) 方式二:查看information_schema.TABLES的信息。 mysql> USE test_schema; mysql> SELECT TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = "test_schema" AND TABLE_NAME = "test_table"; +-----------------+ | TABLE_COLLATION | +-----------------+ | utf8_general_ci | +-----------------+ 方式三:通过SHOW CREATE TABLE确认。 mysql> SHOW CREATE TABLE test_table; +------------+----------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +------------+----------------------------------------------------------------------------------------------------------------+ | test_table | CREATE TABLE `test_table` ( `id` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +------------+----------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) 3、table字符集、字符序如何确定 假设CHARACTER SET、COLLATE的值分别是charset_name、collation_name。如果创建table时: 明确了charset_name、collation_name,则采用charset_name、collation_name。 只明确了charset_name,但collation_name未明确,则字符集采用charset_name,字符序采用charset_name对应的默认字符序。 只明确了collation_name,但charset_name未明确,则字符序采用collation_name,字符集采用collation_name关联的字符集。 charset_name、collation_name均未明确,则采用数据库的字符集、字符序设置。 七、column的字符集、排序 类型为CHAR、VARCHAR、TEXT的列,可以指定字符集/字符序,语法如下: col_name {CHAR | VARCHAR | TEXT} (col_length) [CHARACTER SET charset_name] [COLLATE collation_name] 1、新增column并指定字符集/排序规则 例子如下:(创建table类似) mysql> ALTER TABLE test_table ADD COLUMN char_column VARCHAR(25) CHARACTER SET utf8; 2、查看column的字符集/字符序 例子如下: mysql> SELECT CHARACTER_SET_NAME, COLLATION_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA="test_schema" AND TABLE_NAME="test_table" AND COLUMN_NAME="char_column"; +--------------------+-----------------+ | CHARACTER_SET_NAME | COLLATION_NAME | +--------------------+-----------------+ | utf8 | utf8_general_ci | +--------------------+-----------------+ 1 row in set (0.00 sec) 3、column字符集/排序规则确定 假设CHARACTER SET、COLLATE的值分别是charset_name、collation_name: 如果charset_name、collation_name均明确,则字符集、字符序以charset_name、collation_name为准。 只明确了charset_name,collation_name未明确,则字符集为charset_name,字符序为charset_name的默认字符序。 只明确了collation_name,charset_name未明确,则字符序为collation_name,字符集为collation_name关联的字符集。 charset_name、collation_name均未明确,则以table的字符集、字符序为准。 八、选择:何时设置字符集、字符序 一般来说,可以在三个地方进行配置: 创建数据库的时候进行配置。 mysql server启动的时候进行配置。 从源码编译mysql的时候,通过编译参数进行配置 1、方式一:创建数据库的时候进行配置 这种方式比较灵活,也比较保险,它不依赖于默认的字符集/字符序。当你创建数据库的时候指定字符集/字符序,后续创建table、column的时候,如果不特殊指定,会继承对应数据库的字符集/字符序。 CREATE DATABASE mydb DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci; 2、方式二:mysql server启动的时候进行配置 可以添加以下配置,这样mysql server启动的时候,会对character-set-server、collation-server进行配置。 当你通过mysql client创建database/table/column,且没有显示声明字符集/字符序,那么就会用character-set-server/collation-server作为默认的字符集/字符序。 另外,client、server连接时的字符集/字符序,还是需要通过SET NAMES进行设置。 [mysqld] character-set-server=utf8 collation-server=utf8_general_ci 3、方式三:从源码编译mysql的时候,通过编译参数进行设置 编译的时候如果指定了-DDEFAULT_CHARSET和-DDEFAULT_COLLATION,那么: 创建database、table时,会将其作为默认的字符集/字符序。 client连接server时,会将其作为默认的字符集/字符序。(不用单独SET NAMES) shell> cmake . -DDEFAULT_CHARSET=utf8 \ -DDEFAULT_COLLATION=utf8_general_ci 九、写在后面 本文较为详细地介绍了MySQL中字符集、字符序相关的内容,这部分内容主要针对的是数据的存储与比较。其实还有很重要的一部分内容还没涉及:针对连接的字符集、字符序设置。 由于连接的字符集、字符序设置不当导致的乱码问题也非常多,这部分内容展开来讲内容也不少,放在下一篇文章进行讲解。 篇幅所限,有些内容没有细讲,感兴趣的同学欢迎交流,或者查看官方文档。如有错漏,敬请指出。 十、相关链接 10.1 Character Set Supporthttps://dev.mysql.com/doc/refman/5.7/en/charset.html
简介 网络数据包截获分析工具。支持针对网络层、协议、主机、网络或端口的过滤。并提供and、or、not等逻辑语句帮助去除无用的信息。 tcpdump - dump traffic on a network 例子 不指定任何参数 监听第一块网卡上经过的数据包。主机上可能有不止一块网卡,所以经常需要指定网卡。 tcpdump 监听特定网卡 tcpdump -i en0 监听特定主机 例子:监听本机跟主机182.254.38.55之间往来的通信包。 备注:出、入的包都会被监听。 tcpdump host 182.254.38.55 特定来源、目标地址的通信 特定来源 tcpdump src host hostname 特定目标地址 tcpdump dst host hostname 如果不指定src跟dst,那么来源 或者目标 是hostname的通信都会被监听 tcpdump host hostname 特定端口 tcpdump port 3000 监听TCP/UDP 服务器上不同服务分别用了TCP、UDP作为传输层,假如只想监听TCP的数据包 tcpdump tcp 来源主机+端口+TCP 监听来自主机123.207.116.169在端口22上的TCP数据包 tcpdump tcp port 22 and src host 123.207.116.169 监听特定主机之间的通信 tcpdump ip host 210.27.48.1 and 210.27.48.2 210.27.48.1除了和210.27.48.2之外的主机之间的通信 tcpdump ip host 210.27.48.1 and ! 210.27.48.2 稍微详细点的例子 tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap (1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型 (2)-i eth1 : 只抓经过接口eth1的包(3)-t : 不显示时间戳(4)-s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包(5)-c 100 : 只抓取100个数据包(6)dst port ! 22 : 不抓取目标端口是22的数据包(7)src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24(8)-w ./target.cap : 保存成cap文件,方便用ethereal(即wireshark)分析 抓http包 TODO 限制抓包的数量 如下,抓到1000个包后,自动退出 tcpdump -c 1000 保存到本地 备注:tcpdump默认会将输出写到缓冲区,只有缓冲区内容达到一定的大小,或者tcpdump退出时,才会将输出写到本地磁盘 tcpdump -n -vvv -c 1000 -w /tmp/tcpdump_save.cap 也可以加上-U强制立即写到本地磁盘(一般不建议,性能相对较差) 实战例子 先看下面一个比较常见的部署方式,在服务器上部署了nodejs server,监听3000端口。nginx反向代理监听80端口,并将请求转发给nodejs server(127.0.0.1:3000)。 浏览器 -> nginx反向代理 -> nodejs server 问题:假设用户(183.14.132.117)访问浏览器,发现请求没有返回,该怎么排查呢? 步骤一:查看请求是否到达nodejs server -> 可通过日志查看。 步骤二:查看nginx是否将请求转发给nodejs server。 tcpdump port 8383 这时你会发现没有任何输出,即使nodejs server已经收到了请求。因为nginx转发到的地址是127.0.0.1,用的不是默认的interface,此时需要显示指定interface tcpdump port 8383 -i lo 备注:配置nginx,让nginx带上请求侧的host,不然nodejs server无法获取 src host,也就是说,下面的监听是无效的,因为此时对于nodejs server来说,src host 都是 127.0.0.1 tcpdump port 8383 -i lo and src host 183.14.132.117 步骤三:查看请求是否达到服务器 tcpdump -n tcp port 8383 -i lo and src host 183.14.132.117 相关链接 tcpdump 很详细的http://blog.chinaunix.net/uid-11242066-id-4084382.html http://www.cnblogs.com/ggjucheng/archive/2012/01/14/2322659.htmlLinux tcpdump命令详解 Tcpdump usage examples(推荐)http://www.rationallyparanoid.com/articles/tcpdump.html 使用TCPDUMP抓取HTTP状态头信息http://blog.sina.com.cn/s/blog_7475811f0101f6j5.html
写在前面 在linux的日常管理中,find的使用频率很高,熟练掌握对提高工作效率很有帮助。 find的语法比较简单,常用参数的就那么几个,比如-name、-type、-ctime等。初学的同学直接看第二部分的例子,如需进一步了解参数说明,可以参考find的帮助文档。 find语法如下: find(选项)(参数) 常用例子 根据文件名查找 列出当前目录以及子目录下的所有文件 find . 找到当前目录下名字为11.png的文件 find . -name "11.png" 找到当前目录下所有的jpg文件 find . -name "*.jpg" 找到当前目录下的jpg文件和png文件 find . -name "*.jpg" -o -name "*.png" 找出当前目录下不是以png结尾的文件 find . ! -name "*.png" 根据正则表达式查找 备注:正则表示式比原先想的要复杂,支持好几种类型。可以参考这里 找到当前目录下,文件名都是数字的png文件。 find . -regex "\./*[0-9]+\.png" 根据路径查找 找出当前目录下,路径中包含wysiwyg的文件/路径。 find . -path "*wysiwyg*" 根据文件类型查找 通过-type进行文件类型的过滤。 f 普通文件 l 符号连接 d 目录 c 字符设备 b 块设备 s 套接字 p Fifo 举例,查找当前目录下,路径中包含wysiwyg的文件 find . -type f -path "*wysiwyg*" 限制搜索深度 找出当前目录下所有的png,不包括子目录。 find . -maxdepth 1 -name "*.png" 相对应的,也是mindepth选项。 find . -mindepth 2 -maxdepth 2 -name "*.png" 根据文件大小 通过-size来过滤文件尺寸。支持的文件大小单元如下 b —— 块(512字节) c —— 字节 w —— 字(2字节) k —— 千字节 M —— 兆字节 G —— 吉字节 举例来说,找出当前目录下文件大小超过100M的文件 find . -type f -size +100M 根据访问/修改/变化时间 支持下面的时间类型。 访问时间(-atime/天,-amin/分钟):用户最近一次访问时间。 修改时间(-mtime/天,-mmin/分钟):文件最后一次修改时间。 变化时间(-ctime/天,-cmin/分钟):文件数据元(例如权限等)最后一次修改时间。 举例,找出1天内被修改过的文件 find . -type f -mtime -1 找出最近1周内被访问过的文件 find . -type f -atime -7 将日志目录里超过一个礼拜的日志文件,移动到/tmp/old_logs里。 find . -type f -mtime +7 -name "*.log" -exec mv {} /tmp/old_logs \; 注意:{} 用于与-exec选项结合使用来匹配所有文件,然后会被替换为相应的文件名。 另外,\;用来表示命令结束,如果没有加,则会有如下提示 find: -exec: no terminating ";" or "+" 根据权限 通过-perm来实现。举例,找出当前目录下权限为777的文件 find . -type f -perm 777 找出当前目录下权限不是644的php文件 find . -type f -name "*.php" ! -perm 644 根据文件拥有者 找出文件拥有者为root的文件 find . -type f -user root 找出文件所在群组为root的文件 find . -type f -group root 找到文件后执行命令 通过-ok、和-exec来实现。区别在于,-ok在执行命令前,会进行二次确认,-exec不会。 看下实际例子。删除当前目录下所有的js文件。用-ok的效果如下,删除前有二次确认 find find . -type f -name "*.js" -ok rm {} \; "rm ./1.js"? 试下-exec。直接就删除了 find . -type f -name "*.js" -exec rm {} \; 找出空文件 例子如下 touch {1..9}.txt echo "hello" > 1.txt find . -empty
简介 xargs可以将输入内容(通常通过命令行管道传递),转成后续命令的参数,通常用途有: 命令组合:尤其是一些命令不支持管道输入,比如ls。 避免参数过长:xargs可以通过-nx来将参数分组,避免参数过长。 使用语法如下 Usage: xargs [OPTION]... COMMAND INITIAL-ARGS... Run COMMAND with arguments INITIAL-ARGS and more arguments read from input. 入门例子 首先,创建测试文件 touch a.js b.js c.js 接着,运行如下命令: ls *.js | xargs ls -al 输出如下: -rw-r--r-- 1 a wheel 0 12 18 16:18 a.js -rw-r--r-- 1 a wheel 0 12 18 16:18 b.js -rw-r--r-- 1 a wheel 0 12 18 16:18 c.js 命令解释: 首先,ls *.js的输出为a.js b.js c.js。 通过管道,将a.js b.js c.js作为xargs的输入参数。 xargs命令收到输入参数后,对参数进行解析,以空格/换行作为分隔符,拆分成多个参数,这里变成a.js、b.js、c.js。 xargs将拆分后的参数,传递给后续的命令,作为后续命令的参数,也就是说,组成这样的命令ls -al a.js b.js c.js。 可以加上-t参数,在执行后面的命令前,先将命令打印出来。 ls *.js | xargs -t ls -al 输出如下,可以看到多了一行内容ls -al a.js b.js c.js,这就是实际运行的命令。 ls -al a.js b.js c.js -rw-r--r-- 1 a wheel 0 12 18 16:18 a.js -rw-r--r-- 1 a wheel 0 12 18 16:18 b.js -rw-r--r-- 1 a wheel 0 12 18 16:18 c.js 例子:参数替换 有的时候,我们需要用到原始的参数,可以通过参数-i或-I实现。参数说明如下 -I R same as --replace=R (R must be specified) -i,--replace=[R] Replace R in initial arguments with names read from standard input. If R is unspecified, assume {} 例子如下,将所有的.js结尾的文件,都加上.backup后缀。-I '{}'表示将后面命令行的{}替换成前面解析出来的参数。 ls *.js | xargs -t -I '{}' mv {} {}.backup 展开后的命令如下: mv a.js a.js.backup mv b.js b.js.backup mv c.js c.js.backup 例子:参数分组 命令行对参数最大长度有限制,xargs通过-nx对参数进行分组来解决这个问题。 首先,创建4个文件用来做实验。 touch a.js b.js c.js d.js 然后运行如下命令: ls *.js | xargs -t -n2 ls -al 输出如下,-n2表示,将参数以2个为一组,传给后面的命令。 ls -al a.js b.js -rw-r--r-- 1 root root 0 Dec 18 16:52 a.js -rw-r--r-- 1 root root 0 Dec 18 16:52 b.js ls -al c.js d.js -rw-r--r-- 1 root root 0 Dec 18 16:52 c.js -rw-r--r-- 1 root root 0 Dec 18 16:52 d.js 例子:特殊文件名 有的时候,文件名可能存在特殊字符,比如下面的文件名中存在空格。 touch 'hello 01.css' 'hello 02.css' 运行之前的命令会报错,因为xargs是以空格/换行作为分隔符,于是就会出现预期之外的行为。 # 命令 find . -name '*.css' | xargs -t ls -al #输出 ls -al ./hello 01.css ./hello 02.css # 展开后的命令 ls: cannot access ./hello: No such file or directory ls: cannot access 01.css: No such file or directory ls: cannot access ./hello: No such file or directory ls: cannot access 02.css: No such file or directory xargs是这样解决这个问题的。 -print0:告诉find命令,在输出文件名之后,跟上NULL字符,而不是换行符; -0:告诉xargs,以NULL作为参数分隔符; find . -name '*.css' -print0 | xargs -0 -t ls -al 例子:日志备份 将7天前的日志备份到特定目录 find . -mtime +7 | xargs -I '{}' mv {} /tmp/otc-svr-logs/ 相关链接 https://craftsmanbai.gitbooks.io/linux-learning-wiki/content/xargs.html http://wiki.jikexueyuan.com/project/shell-learning/xargs.html