修正BSDライセンスでかなり良い感じのTCP/IPプロトコルスタック。 MinixもVirtualBoxも使っている様子。主な実装済みプロトコルは以下の通り。
- DNS, DHCP
- TCP, UDP
- IPv4, IPv6, ICMP
- ARP
移植はかなり簡単で以下のものを用意すれば OSのTCP/IPプロトコルスタックとしては十分に動いてくれる。
int memcmp(const void *s1, const void *s2, size_t n);
void *memcpy(void *dest, const void *src, size_t n);
void *memset(void *s, int c, size_t n);
size_t strlen(const char *s);
malloc()
がないのは、lwIPが.data
セクションかどこかに
メモリプールを用意して自前でやりくりするため。
sys_now()
は現在時刻(ミリ秒)を返す。時刻の差をとるために使うので
ラップアラウンドへの配慮は不要。定数返すだけでも動くが、その実装だとたぶん
TCP timestampとかタイムアウトとかが動かないと思う。
/* lwipopts.h: 自分でファイルを作る */
#define NO_SYS 1
/* cc.h */
#define LWIP_PLATFORM_DIAG(x) printf // デバッグ情報
#define LWIP_PLATFORM_ASSERT(x) printf // assertion失敗
NICのドライバはsrc/netif/ethernetif.c
に雛形があるのでそれを以下の
ように修正する。下の2つはソースコードに丁寧なコメントが載っている。
low_level_init()
:netif->hwaddr
にMACアドレスを詰める。ethernetif_input()
: パケットが来たら、これを呼ぶ。引数にはそのNICのnetif
。low_level_output()
: パケットをひとつずつ真心込めて送信low_level_input()
: 受信したパケットをバッファに詰めていく。ethernetif_input()
から呼ばれる。
マルチスレッドサポート、BSDソケットのエミュレーションをlwIPに
やってもらうにはSemaphoreとかも必要。参照: doc/sys_arch.txt
/* たぶんこんなにインクルードしなくていい */
#include <lwip/opt.h>
#include <lwip/stats.h>
#include <lwip/sys.h>
#include <lwip/pbuf.h>
#include <lwip/udp.h>
#include <lwip/tcp.h>
#include <lwip/dns.h>
#include <lwip/dhcp.h>
#include <lwip/init.h>
#include <lwip/netif.h>
#include <netif/etharp.h>
static struct netif netif;
void setup(void) {
struct ip_addr ipaddr;
struct ip_addr netmask;
struct ip_addr gateway;
/* DHCPも対応しているが、ここでは静的に割り当てる。 */
IP4_ADDR(&netmask, 255, 255, 255, 255);
IP4_ADDR(&gateway, 10, 0, 2, 2);
IP4_ADDR(&ipaddr, 10, 0, 2, 15);
lwip_init(); // いろいろ初期化
/* NICを登録 */
netif_add(&netif, &ipaddr, &netmask, &gateway, NULL,
(netif_init_fn) ethernetif_init, ethernet_input);
/*
* デフォルトルートに設定する
*
* lwIPは複数NICに対応しており、IPパケット送信時に
* ip_route() でどのNICから送るべきかを決定されるが、
* 送信先がどのNICのセグメントでもない場合に、これで
* 設定されたNICが選ばれる。
*
*/
netif_set_default(&netif);
/* NICの有効化: 準備完了なことをlwIPに伝える */
netif_set_up(&netif);
}
エラー処理は全くしていない。パケットの送受信はサーバの方を参照。
/* データを受信した時に呼ばれる */
static err_t recv(void *arg, struct tcp_pcb *pcb,
struct pbuf *pbuf, err_t err) {
/* 受信したものは pbuf の中 */
puts("received!");
return ERR_OK;
}
/* データがきちんと送信できた時に呼ばれる */
err_t sent (void *arg, struct tcp_pcb *pcb, u16_t len){
return ERR_OK;
}
/* コネクションが張られたときに呼ばれる */
err_t connected(void *arg, struct tcp_pcb *pcb, err_t err) {
tcp_recv(pcb, recv); // 受信した時のハンドラを設定
tcp_sent(pcb, sent); // 送信できた時のハンドラを設定
/*
* ここでパケットを送る
*
* tcp_write() 送信バッファにデータを詰める
* tcp_output() 送信開始
*
*/
return ERR_OK;
}
void main(void) {
struct tcp_pcb *pcb;
struct ip_addr ipaddr;
u16_t port;
IP4_ADDR(&ipaddr, 130, 158, 87, 1); /* www.coins.tsukuba.ac.jp */
port = 80;
pcb = tcp_new();
tcp_connect(pcb, &ipaddr, port, connected);
}
たぶん動くechoサーバ。クライアントと同じく、エラー処理はしていない。
err_t recv(void *arg, struct tcp_pcb *pcb,
struct pbuf *pbuf, err_t err) {
if (pbuf != NULL) {
char buf[512];
int len;
/* 受信したものを受け取る */
len = (pbuf->tot_len < sizeof(buf))? pbuf->tot_len : sizeof(buf);
memcpy(&buf, pbuf->payload, len);
tcp_recved(pcb, pbuf->tot_len); // 受け取ったことを通知
pbuf_free(pbuf); // pbuf開放
/* 適当に返信を送る */
tcp_write(pcb, len, size, 1);
tcp_output(pcb);
}else{
tcp_close(pcb);
}
return ERR_OK;
}
/* クライアントからコネクションを張られたときに呼ばれる */
err_t accept(void *arg, struct tcp_pcb *pcb, err_t err) {
tcp_setprio(pcb, TCP_PRIO_MIN); // 優先度を設定
tcp_recv(pcb, recv); // パケットを受信した時のハンドラを設定
tcp_err(pcb, NULL); // 何か起きた時のハンドラを設定
tcp_poll(pcb, NULL, 4); // インターバルごとに呼ばれるハンドラを設定
return ERR_OK;
}
void main(void) {
struct tcp_pcb *pcb;
pcb = tcp_new();
/* bind */
tcp_bind(pcb, IP_ADDR_ANY /* IPアドレス */, 80 /* ポート番号 */);
/* listen: 注意点として、pcbが新しく割り当てられて戻ってくるので
戻り値をきちんと拾ってあげる必要がある */
pcb = tcp_listen(pcb);
/* accept: これはブロックしない */
tcp_accept(server->pcb, accept_handler);
/* パケットを待ち続ける */
for (;;);
}
- APIはちゃんとマニュアル・ソースコードを読む。BSDソケットと同じ名前でも 振る舞いが全く異なる場合がある。
- lwIPのassertionが失敗するのは、ほとんどの場合で使い方 or NICのドライバが間違ってるのが原因。