目錄
RabbitMQ-C使用開發詳解(Windows環境)
一、概述
討論的是windows環境下的使用RabbitMQ-c與RabbitMQ服務端的交互。
二、編譯RabbitMQ-c
RabbitMq的C/C++客戶端有很多,我們選用RabbitMq-c。windows環境下的MFC開發,需要把RabbitMq-c客戶端編譯成dll。
1.下載和安裝
下載rabbitmq-c最新代碼包:https://github.com/alanxz/rabbitmq-c
下載cmake最新安裝包:https://cmake.org/download/
2.使用cmake編譯生成適合自己編譯環境的工程
第一步:填寫源代碼路徑
第二步:填寫建立後的路徑,build的文件夾一般建立在源代碼路徑裏,也可以放在其他位置
第三步:點擊配置按鈕,在配置裏面選擇屬於自己編譯環境的名字
第四步:點擊生成按鈕,不出現運行失敗就說明已經編譯成功了
特別說明:在編譯rabbitmq-c是如果出現如圖的錯誤,可以去掉ENABLE_SSL_SUPPORT括號裏的對勾。
在以上生成的工程目錄下的librabbitmq\Debug路徑下會生成librabbitmq.4.lib、librabbitmq.4.dll兩個文件,對應的動態庫的導出文件在rabbitmq-c-master\librabbitmq目錄下。
三、核心原理
3.1生產者與交換機關係
生產者與交換機的關係是多對多的有關係,多個生產者可以給同一個交換機生產消息,同時一個生產者也可以能多個交換機生產消息。
3.2交換機與隊列關係
交換機與隊列的關係是多對多的關係,一個交換機可以給多個隊列提供消息,同時多個交換機也可以同時給一個隊列提供消息。
3.3隊列與消費者關係
隊列與消費者的關係是多對多的關係,一個隊列可以同時被多個消費者消費,同時一個消費者可以同時消費多個隊列的消息。
3.4交換機與交換機的關係
交換機與交換機的關係,與交換機與隊列的關係是一樣的。部份交換機充當隊列的角色,從其綁定的交換機上分流數據,然後再把自己角色轉換成交換機,然後給綁定在自身上的隊列分派消息。
所以交換機之間的關係是多對多,一個上級交換機可以綁定多個下級交換機,一個下級交換機可同時綁定多個上級交換機。
四、開發者
4.1接口文件
總共有4個導出文件:
amqp.h:主要的rabbitmq-c客戶端接口都在此文件
amqp_tcp_socket.h:與socket相關的接口
mqp_framing.h:不常用的一些接口
amqp_ssl_socket.h:用戶ssl方式加密訪問rabbitmq-server
生產者生產消息過程:
(1)客戶端連接到消息隊列服務器,打開一個channel。
(2)客戶端聲明一個exchange,並設置相關屬性。
(3)客戶端聲明一個queue,並設置相關屬性。
(4)客戶端使用routing key,在exchange和queue之間建立好綁定關係。
(5)客戶端投遞消息到exchange。
RabbitMQ支持消息的持久化:
也就是數據寫在磁盤上,爲了數據安全考慮,我想大多數用戶都會選擇持久化。如果exchange和queue都是持久化的,那麼它們之間的binding也是持久化的。如果exchange和queue兩者之間有一個持久化,一個非持久化,就不允許建立綁定。消息隊列持久化包括3個部分:
(1)exchange持久化,在聲明時指定durable => 1
(2)queue持久化,在聲明時指定durable => 1
(3)消息持久化,在投遞時指定delivery_mode=> 2(1是非持久化)
4.2交換機
聲明交換機
AMQP_PUBLIC_FUNCTION amqp_exchange_declare_ok_t *AMQP_CALL amqp_exchange_declare(amqp_connection_state_t state, amqp_channel_t channel,amqp_bytes_t exchange, amqp_bytes_t type, amqp_boolean_t passive,amqp_boolean_t durable, amqp_boolean_t auto_delete, amqp_boolean_t internal,amqp_table_t arguments);
/**
* amqp_exchange_declare
*
* @param [in] connect連接 amqp_new_connection獲取
* @param [in] channel the channel to do the RPC on,程序自己設置一個通道號,一個連接可以多個通道號。
* @param [in] exchange 指定交換機名稱 eg:amqp_cstring_bytes("exchange_cat")
* @param [in] type 指定交換機類型,amqp_cstring_bytes("direct")
* "fanout" 廣播的方式,發送到該exchange的所有隊列上。
* "direct" 通過路由鍵發送到指定的隊列上。
* "topic" 通過匹配路由鍵的方式獲取,使用通配符*,#
* @param [in] passive 檢測exchange是否存在,設爲true,若exchange存在則命令成功返回(調用其他參數不會影響exchange屬性),若不存在不會創建exchange,返回錯誤。設爲false,如果exchange不存在則創建exchange,調用成功返回。如果exchange已經存在,並且匹配現在exchange的話則成功返回,如果不匹配則exchange聲明失敗。
* @param [in] durable 隊列是否持久化
* @param [in] auto_delete 連接斷開的時候,exchange是否自動刪除
* @param [in] internal internal
* @param [in] arguments arguments
* @returns amqp_exchange_declare_ok_t
*/
Demo示例:
#include <amqp.h>
#include <amqp_tcp_socket.h>
void die_on_amqp_error2(amqp_rpc_reply_t x, char const *context) {
char sLog[1024] = {0};
switch (x.reply_type) {
case AMQP_RESPONSE_NORMAL:
return;
case AMQP_RESPONSE_NONE:
printf(sLog, "%s: missing RPC reply type!\n", context);
break;
case AMQP_RESPONSE_LIBRARY_EXCEPTION:
printf(sLog, "%s: %s\n", context, amqp_error_string2(x.library_error));
break;
case AMQP_RESPONSE_SERVER_EXCEPTION:
switch (x.reply.id) {
case AMQP_CONNECTION_CLOSE_METHOD: {
amqp_connection_close_t *m =
(amqp_connection_close_t *)x.reply.decoded;
printf(sLog, "%s: server connection error %uh, message: %.*s\n",
context, m->reply_code, (int)m->reply_text.len,
(char *)m->reply_text.bytes);
break;
}
case AMQP_CHANNEL_CLOSE_METHOD: {
amqp_channel_close_t *m = (amqp_channel_close_t *)x.reply.decoded;
printf(sLog, "%s: server channel error %uh, message: %.*s\n",
context, m->reply_code, (int)m->reply_text.len,
(char *)m->reply_text.bytes);
break;
}
default:
printf(sLog, "%s: unknown server error, method id 0x%08X\n",
context, x.reply.id);
break;
}
break;
}
AfxMessageBox(sLog);
}
void die_on_error2(int x, char const *context) {
if (x < 0) {
char sLog[1024] = {0};
printf(sLog, "%s: %s\n", context, amqp_error_string2(x));
AfxMessageBox(sLog);
}
}
void Crabbitmq_demoDlg::OnBnClickedButton1()
{
char const *hostname;
int port, status;
char const *exchange;
char const *exchangetype;
amqp_socket_t *socket = NULL;
amqp_connection_state_t conn;
hostname ="localhost";
port = 5672;
exchange = "test.fanout";
exchangetype = "fanout"; //fanout/direct/topic
conn = amqp_new_connection();
socket = amqp_tcp_socket_new(conn);
if (!socket) {
AfxMessageBox("creating TCP socket");
}
status = amqp_socket_open(socket, hostname, port);
if (status) {
AfxMessageBox("opening TCP socket");
}
die_on_amqp_error2(amqp_login(conn, "/", 0, 131072, 0, AMQP_SASL_METHOD_PLAIN,
"guest", "guest"),
"Logging in");
amqp_channel_open(conn, 1);
die_on_amqp_error2(amqp_get_rpc_reply(conn), "Opening channel");
amqp_exchange_declare(conn, 1, amqp_cstring_bytes(exchange),
amqp_cstring_bytes(exchangetype), 0, 0, 0, 0,
amqp_empty_table);
die_on_amqp_error2(amqp_get_rpc_reply(conn), "Declaring exchange");
die_on_amqp_error2(amqp_channel_close(conn, 1, AMQP_REPLY_SUCCESS),
"Closing channel");
die_on_amqp_error2(amqp_connection_close(conn, AMQP_REPLY_SUCCESS),
"Closing connection");
die_on_error2(amqp_destroy_connection(conn), "Ending connection");
int end = 0;
}
刪除交換機
/**
* amqp_exchange_delete
*
* @param [in] state connection state
* @param [in] channel the channel to do the RPC on
* @param [in] exchange exchange
* @param [in] if_unused if_unused
* @returns amqp_exchange_delete_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_delete_ok_t *AMQP_CALL
amqp_exchange_delete(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t exchange, amqp_boolean_t if_unused);
向交換機綁定路由鍵
隊列的消息來源於交換機,所以隊列需要通過路由鍵與交換機建立聯繫,然後報備自己需要的消息。
/**
* amqp_queue_bind
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:待綁定的隊列名稱
* @param [in] exchange exchange:與隊列綁定的交換機名稱
* @param [in] routing_key routing_key:路由鍵
* @param [in] arguments arguments
* @returns amqp_queue_bind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_bind_ok_t *AMQP_CALL amqp_queue_bind(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_table_t arguments);
從交換機解綁路由鍵
取消隊列與交換機之間的關聯。
/**
* amqp_queue_unbind
*
* @param [in] state connection state:對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:隊列名稱
* @param [in] exchange exchange:交換機名稱
* @param [in] routing_key routing_key:路由鍵
* @param [in] arguments arguments
* @returns amqp_queue_unbind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_unbind_ok_t *AMQP_CALL amqp_queue_unbind(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_table_t arguments);
交換機之間綁定路由鍵
把隊列與交換機,通過路由鍵建立交聯,當交換機收到指定路由鍵的消息時,交會路由到之前綁定的隊列中去。
/**
* amqp_exchange_bind
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] destination destination:接收消息的交換機
* @param [in] source source:發出消息的交換機
* @param [in] routing_key routing_key:路由鍵
* @param [in] arguments arguments
* @returns amqp_exchange_bind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_bind_ok_t *AMQP_CALL
amqp_exchange_bind(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t destination, amqp_bytes_t source,
amqp_bytes_t routing_key, amqp_table_t arguments);
交換機之間解綁路由鍵
把隊列與交換機通過路由鍵建立的交聯進行解除,讓隊列與交換機解除關係。
/**
* amqp_exchange_unbind
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] destination destination:
* @param [in] source source
* @param [in] routing_key routing_key
* @param [in] arguments arguments
* @returns amqp_exchange_unbind_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_exchange_unbind_ok_t *AMQP_CALL
amqp_exchange_unbind(amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t destination, amqp_bytes_t source,
amqp_bytes_t routing_key, amqp_table_t arguments);
4.3隊列
聲明隊列
/**
* amqp_queue_declare
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:需要綁定的隊列名稱
* @param [in] passive passive:檢測queue是否存在,設爲true,若queue存在則命令成功返回(調用其他參數不會影響queue屬性),若不存在不會創建queue,返回錯誤。設爲false,如果queue不存在則創建queue,調用成功返回。如果queue已經存在,並且匹配現在queue的話則成功返回,如果不匹配則queue聲明失敗。
* @param [in] durable durable:是否持久化(寫入到硬盤)
* @param [in] exclusive exclusive:當前連接斷開時,隊列是否自動刪除
* @param [in] auto_delete auto_delete:沒有消費者時,是否自動刪除
* @param [in] arguments arguments
* @returns amqp_queue_declare_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_declare_ok_t *AMQP_CALL amqp_queue_declare(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_boolean_t passive, amqp_boolean_t durable, amqp_boolean_t exclusive,
amqp_boolean_t auto_delete, amqp_table_t arguments);
清空隊列
清空隊列裏的數據
/**
* amqp_queue_purge
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:隊列名稱
* @returns amqp_queue_purge_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_purge_ok_t *AMQP_CALL amqp_queue_purge(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_bytes_t queue);
刪除隊列
/**
* amqp_queue_delete
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道號
* @param [in] queue queue:隊列名稱
* @param [in] if_unused if_unused:當爲真時,僅當隊列不使用時刪除
* @param [in] if_empty if_empty:當爲真時,僅當隊列爲空時刪除
* @returns amqp_queue_delete_ok_t:返回值
*/
AMQP_PUBLIC_FUNCTION
amqp_queue_delete_ok_t *AMQP_CALL amqp_queue_delete(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_boolean_t if_unused, amqp_boolean_t if_empty);
4.4生產者
/**
* 發佈一條消息到broker
*
* 使用路由密鑰在exchange上發佈消息。
*
* 請注意,在AMQ協議級別basic.publish是一個異步方法:
* 這意味着broker發生的錯誤情況(例如發佈到不存在的exchange)將不會反映在此函數的返回值中。
*
* \param [in] state 連接對象
* \param [in] channel 通道標識符
* \param [in] exchange broker需要發佈到的exchange
* \param [in] routing_key 發佈消息使用的路由祕鑰
* \param [in] mandatory 向broker表明該消息必須路由到一個隊列。如果broker不能這樣做,
* 它應該用一個basic.return方法來回應。
* \param [in] immediate 向broker表明該消息必須立即傳遞給消費者,如果broker不能這樣做,
* 它應該用一個basic.return方法來回應。
* \param [in] properties 與消息相關的屬性
* \param [in] body 消息體
* \return 成功返回AMQP_STATUS_OK,失敗返回amqp_status_enum值
* 注意:請注意,basic.publish是一個異步方法,此函數的返回值僅指示消息數據已
* 成功傳輸到代理.它並不表示broker發生的故障,例如發佈到不存在的exchange.
* 可能的錯誤值:
* - AMQP_STATUS_TIMER_FAILURE:系統計時器設施返回錯誤,消息未被髮送。
* - AMQP_STATUS_HEARTBEAT_TIMEOUT: 等待broker的心跳連接超時,消息未被髮送
* - AMQP_STATUS_NO_MEMORY:分配內存失敗,消息未被髮送
* - AMQP_STATUS_TABLE_TOO_BIG:屬性中的table太大而不適合單個框架,消息未被髮送
* - AMQP_STATUS_CONNECTION_CLOSED:連接被關閉。
* - AMQP_STATUS_SSL_ERROR:發生SSL錯誤。
* - AMQP_STATUS_TCP_ERROR:發生TCP錯誤,errno或WSAGetLastError()可能提供更多的信息
*
*/
AMQP_PUBLIC_FUNCTION
int AMQP_CALL amqp_basic_publish(
amqp_connection_state_t state, amqp_channel_t channel,
amqp_bytes_t exchange, amqp_bytes_t routing_key, amqp_boolean_t mandatory,
amqp_boolean_t immediate, struct amqp_basic_properties_t_ const *properties,
amqp_bytes_t body);
4.5消費者
消費方式分push與pull。RabbitMQ-C的example和網上的博客都是通過consume方式獲取數據,但consume是通過推送的方式被動消費數據,當調用amqp_basic_consume開始一個消費者後,服務器就開始推送數據,而調用amqp_consume_message只是從本地的緩衝區中讀取數據,每次服務器都推送了300條數據;
而我想一條一條的主動取,example中並沒有關於主動get數據的方式。後來經過多次讀源碼,寫例子,才發現amqp_basic_get就是一條一條的取數據,當然這個函數不能像amqp_basic_consume一樣僅僅調用一次;而是在每次amqp_read_message之前都要調用amqp_basic_get方法;
如果只調用一次amqp_basic_get那麼當我們第二次調用amqp_read_message的時候就會阻塞,所以它們必須成對出現。
4.5.1訂閱
服務端主動給消費者推送消息,通過兩個函數配合使用,先調用amqp_basic_consume開始一個消費者後,服務器就開始推送數據,再調用amqp_consume_message從本地的緩衝區中讀取數據,每次服務器都推送了300條數據。
/**
* amqp_basic_consume:開始一個隊列消費者
*
* @param [in] state connection state:連接對象
* @param [in] channel the channel to do the RPC on:通道
* @param [in] queue queue:隊列名稱
* @param [in] consumer_tag consumer_tag:消費者標籤
* @param [in] no_local no_local:填false,屬於amqp標準,rabbitmq沒有實現
* @param [in] no_ack no_ack:如果爲true,消費消息後直接從隊列清除,爲false,需要
* 人工調用amqp_basic_ack函數後,消息纔會從隊列清除。
* @param [in] exclusive exclusive:排他消費者,即這個隊列只能由一個消費者消費.適用於
* 任務不允許進行併發處理的情況下.比如系統對接
* @param [in] arguments arguments
* @returns amqp_basic_consume_ok_t
*/
AMQP_PUBLIC_FUNCTION
amqp_basic_consume_ok_t *AMQP_CALL amqp_basic_consume(
amqp_connection_state_t state, amqp_channel_t channel, amqp_bytes_t queue,
amqp_bytes_t consumer_tag, amqp_boolean_t no_local, amqp_boolean_t no_ack,
amqp_boolean_t exclusive, amqp_table_t arguments);
/**
* Wait for and consume a message:等待並消費消息
*
* Waits for a basic.deliver method on any channel, upon receipt of
* basic.deliver it reads that message, and returns. If any other method is
* received before basic.deliver, this function will return an amqp_rpc_reply_t
* with ret.reply_type == AMQP_RESPONSE_LIBRARY_EXCEPTION, and
* ret.library_error == AMQP_STATUS_UNEXPECTED_STATE. The caller should then
* call amqp_simple_wait_frame() to read this frame and take appropriate action.
*
* This function should be used after starting a consumer with the
* amqp_basic_consume() function
*
* \param [in,out] state the connection object:連接對象
* \param [in,out] envelope a pointer to a amqp_envelope_t object. Caller
* should call #amqp_destroy_envelope() when it is done using
* the fields in the envelope object. The caller is responsible
* for allocating/destroying the amqp_envelope_t object itself.
*返回一個指針,指向一個對象,這個對象使用後,需要人工調用amqp_destroy_envelope
*釋放。
* \param [in] timeout a timeout to wait for a message delivery. Passing in
* NULL will result in blocking behavior.:等待時間,爲NULL將阻塞
* \param [in] flags pass in 0. Currently unused.:無效
* \returns a amqp_rpc_reply_t object. ret.reply_type == AMQP_RESPONSE_NORMAL
* on success. If ret.reply_type == AMQP_RESPONSE_LIBRARY_EXCEPTION,
* and ret.library_error == AMQP_STATUS_UNEXPECTED_STATE, a frame other
* than AMQP_BASIC_DELIVER_METHOD was received, the caller should call
* amqp_simple_wait_frame() to read this frame and take appropriate
* action.
*
* \since v0.4.0
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_consume_message(amqp_connection_state_t state,
amqp_envelope_t *envelope,
struct timeval *timeout,
int flags);
4.5.2拉取
amqp_basic_get與amqp_read_message配合,一條一條主動提取消息。如果只調用一次amqp_basic_get那麼當我們第二次調用amqp_read_message的時候就會阻塞,所以它們必須成對出現。
/**
* Do a basic.get:單條提取消息
*
* Synchonously polls the broker for a message in a queue, and
* retrieves the message if a message is in the queue.:從隊列中提取消息,一次提取一
*條,這是主動提取,適合某些主動處理消息的場景
* \param [in] state the connection object:連接對象
* \param [in] channel the channel identifier to use:通道
* \param [in] queue the queue name to retrieve from:隊列
* \param [in] no_ack if true the message is automatically ack'ed
* if false amqp_basic_ack should be called once the message
* retrieved has been processed:是否要人工回饋
* \return amqp_rpc_reply indicating success or failure
*
* \since v0.1
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_basic_get(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_bytes_t queue,
amqp_boolean_t no_ack);
/**
* Reads the next message on a channel:讀取消息
*讀取指定通道上的隊列上的消息,需要與amqp_basic_get()配合使用
* Reads a complete message (header + body) on a specified channel. This
* function is intended to be used with amqp_basic_get() or when an
* AMQP_BASIC_DELIVERY_METHOD method is received.
*
* \param [in,out] state the connection object:連接對象
* \param [in] channel the channel on which to read the message from:通道
* \param [in,out] message a pointer to a amqp_message_t object. Caller should
* call amqp_message_destroy() when it is done using the
* fields in the message object. The caller is responsible for
* allocating/destroying the amqp_message_t object itself.
* 指向返回消息的指針,需要人工調用amqp_message_destroy() 接口釋放
* \param [in] flags pass in 0. Currently unused.:廢棄
* \returns a amqp_rpc_reply_t object. ret.reply_type == AMQP_RESPONSE_NORMAL on
* success.
*
* \since v0.4.0
*/
AMQP_PUBLIC_FUNCTION
amqp_rpc_reply_t AMQP_CALL amqp_read_message(amqp_connection_state_t state,
amqp_channel_t channel,
amqp_message_t *message,
int flags);
/**
* Acknowledges a message:回饋指定消息
*
* Does a basic.ack on a received message:對消費後的消息進行回饋
*
* \param [in] state the connection object:連接對象
* \param [in] channel the channel identifier:通道
* \param [in] delivery_tag the delivery tag of the message to be ack'd:傳輸標籤
* \param [in] multiple if true, ack all messages up to this delivery tag, if
* false ack only this delivery tag:傳輸標籤
* \return 0 on success, 0 > on failing to send the ack to the broker.
* this will not indicate failure if something goes wrong on the
* broker
*
* \since v0.1
*/
AMQP_PUBLIC_FUNCTION
int AMQP_CALL amqp_basic_ack(amqp_connection_state_t state,
amqp_channel_t channel, uint64_t delivery_tag,
amqp_boolean_t multiple);
delivery_tag如何獲得?以下是僞代碼:
amqp_rpc_reply_t rpc_reply;
do {
rpc_reply = amqp_basic_get(conn, 1,queuename, 0);
} while (rpc_reply.reply_type == AMQP_RESPONSE_NORMAL &&
rpc_reply.reply.id == AMQP_BASIC_GET_EMPTY_METHOD
/*&& amqp_time_has_past(deadline) == AMQP_STATUS_OK*/);
amqp_message_t message;
amqp_rpc_reply_t rpc_reply2 = amqp_read_message(conn, 1, &message, 0);
amqp_method_t method = rpc_reply.reply;
if (AMQP_BASIC_GET_OK_METHOD == method.id)
{
amqp_basic_ack_t *s;
s = (amqp_basic_ack_t *) method.decoded;
int nRe = amqp_basic_ack(conn, 1, s->delivery_tag, 0);
int i = 0;
}
而通過amqp_consume_message接口消費的消息,如果要回饋的話,delivery_tag在返回的amqp_envelope_t變量裏。