---
# System prepended metadata

title: 網路程式設計 HW11 多緒網路程式作業

---

# 網路程式設計 HW11 多緒網路程式作業

學生: 1084837廖經翰

## 進階練習1. Echo Server多工

這次的作業進階練習1是利用thread來做可以多工的server，server最多可以讓10個client連線，每當有一個client連進來的時候，server就會用一個thread來服務該client，來達到多工的方式，並且還會顯示哪一個thread會去服務哪一個client，還有顯示server目前用thread服務幾個client

( client.c 程式只有一個，可以開多次來模擬多個client )

執行畫面:

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/rJiHxMIrh.png" /></center>

<center>server</center>

<br>

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/S108eGIS3.png" /></center>

<center>client</center>

<br>

server程式碼:
```c=
#include<stdio.h>
#include<winsock.h>
#include<stdlib.h>

#define MaxClient 10

void *thread_main(void *arg);

//thread的結構
struct Thread_Args
{
    //存client的位子
    int clnt_socket[MaxClient];

    //存第幾個client
    int clnt_index;

    //存共有幾個client
    int clnt_total;

    //存該socket
    int sock;
};

int main()
{
    //thread結構
    struct Thread_Args *thread_arg;

    //thread id
    DWORD thread_id;

    //set WSA and initial WSAStartup
	WSADATA wsadata;

	struct sockaddr_in server_addr;

	struct sockaddr_in client_addr;

    int serv_sock , clnt_socket;

	int i , n , client_len;

    //配置記憶體給thread結構
	thread_arg = (struct Thread_Args *) malloc( sizeof(struct Thread_Args) );

    //初始化全部的client位子
	for( int i = 0 ; i  < MaxClient ; i++ )
    {
        thread_arg -> clnt_socket[i] = 0;
    }

    //初始化存共有幾個client的變數
    thread_arg -> clnt_total = 0;

    //呼叫WSAStartup()註冊WinSock DLL的使用
	if( WSAStartup( 0x101 , &wsadata ) != 0 )
    {
        printf( "WSAStartup() failed" );

        exit(1);
    }

	//使用TCP協定
	if( ( serv_sock = socket( PF_INET , SOCK_STREAM , IPPROTO_TCP) ) < 0 )
    {
        printf( "socket() failed" );
    }

    //初始化server
    memset( &server_addr , 0 , sizeof(server_addr) );

    //設定server的結構
	server_addr.sin_addr.s_addr = htonl( INADDR_ANY );
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons( 5678 );

    //綁定server結構裡設定的資訊
	if( (bind( serv_sock , (struct sockaddr*) &server_addr , sizeof(server_addr) )) < 0 )
    {
        printf( "bind() failed" );
    }

    if( listen( serv_sock , 3 ) < 0 )
    {
        printf( "listen() failed" );
    }

    printf( "server is waiting for clients...\n" );

    while(1)
    {
        client_len = sizeof( client_addr );

        //當如果有client進來就處理該client，就處理該client
        for( i = 0 ; i < MaxClient ; i++ )
        {
            if( thread_arg -> clnt_socket[i] == 0 )
            {
                break;
            }
        }

        //紀錄該client是第幾個人
        thread_arg -> clnt_index = i;

        if( ( clnt_socket = accept( serv_sock , (struct sockaddr*) &client_addr , &client_len ) ) <= 0 )
        {
            printf( "accept() failed" );
        }

        //把連線的socket分配給thread結構的sock，之後要傳到function裡面
        thread_arg -> sock = clnt_socket;

        //把連線的socket分配給thread結構裡client的位子
        thread_arg -> clnt_socket[i] = clnt_socket;

        //有client人進來，總人數加一
        thread_arg -> clnt_total++;

        //建立一個thread來服務該client
        if( CreateThread( NULL , 0 , (LPTHREAD_START_ROUTINE) thread_main , thread_arg , 0 , (LPDWORD) &thread_id ) == NULL )
        {
            printf( "CreateThread() failed" );
        }

        printf( "New client with thread id: %1d and socket: %d ( Current client: %d )\n" , thread_id , thread_arg -> clnt_socket[i] , thread_arg -> clnt_total );
    }

    //關閉TCP socket
	closesocket( serv_sock );

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}

void *thread_main( void *thread_arg )
{
    int client_idx , clnt_sock_i , recv_msg_size;

    char buffer[1000];

    //紀錄該client的socket
    clnt_sock_i = ( (struct Thread_Args *) thread_arg ) -> sock;

    //紀錄該client是第幾個人
    client_idx = ( (struct Thread_Args *) thread_arg ) -> clnt_index;

    while(1)
    {
        //接收資訊
        recv_msg_size = recv( clnt_sock_i , buffer , 1000 , 0 );

        //如果有人中斷連線或是離開
        if( recv_msg_size <= 0 )
        {
            //總人數減一
            ( (struct Thread_Args *) thread_arg ) -> clnt_total--;

            //初始化該client的位子
            ( (struct Thread_Args *) thread_arg ) -> clnt_socket[client_idx] = 0;

            printf( "connection[ socket: %d ] closed... ( Current client: %d )\n" , clnt_sock_i , ( (struct Thread_Args *) thread_arg ) -> clnt_total );

            break;
        }

        //傳送資訊
        send( clnt_sock_i , buffer , strlen(buffer)+1 , 0 );
    }

    //關閉該client的socket
    closesocket( clnt_sock_i );

    //初始化該client的位子
    ( (struct Thread_Args *) thread_arg ) -> clnt_socket[client_idx] = 0;

    return( NULL );
}
```

client程式碼:
```c=
#include<stdio.h>
#include<string.h>
#include<winsock.h>

#define MAXLINE 1024

int main()
{
    //set WSA and initial WSAStartup
	SOCKET sd;
	WSADATA wsadata;

	struct sockaddr_in serv;

	int i , n;

	char str[100] = "I love NP!";

	//呼叫WSAStartup()註冊WinSock DLL的使用
	WSAStartup( 0x101 , (LPWSADATA) &wsadata );

	//使用TCP協定
	sd = socket( AF_INET , SOCK_STREAM , 0 );

	//設定server的結構
	serv.sin_addr.s_addr = inet_addr( "127.0.0.1" );
	serv.sin_family = AF_INET;
	serv.sin_port = htons( 5678 );

	//連接至server
	connect( sd , (LPSOCKADDR) &serv , sizeof(serv) );

    while(1)
    {
        strcpy( str , "How are you?" );

        //傳送訊息給server
        n = send( sd , str , strlen(str)+1 , 0 );

        printf( "send: %s\n" , str );

        if( n < 0 )
        {
            break;
        }

        //緩衝時間1秒
        Sleep( 1000 );

        //接收訊息
        n = recv( sd , str , MAXLINE , 0 );

        if( n <= 0 )
        {
            break;
        }

        printf( "recv: %s\n" , str );
    }

    //關閉TCP socket
	closesocket(sd);

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}
```

## 進階練習2. 聊天室Server多工

作業進階練習2是利用thread來做可以多工的server聊天室，也就是server可以利用thread來創建一間聊天室，並且可以同時創建很多間聊天室來達到多工的功能，一間聊天室可以服務兩個client，所以client.c會模擬兩個client來測試，每當執行client.c的時候就會有兩個client連入server，這樣server就用一個thread來服務這兩個client變成一個聊天室，並且可以同時開很多聊天室，為了要讓client可以自由發言，所以使用之前教的非阻攔模式功能來讓client沒有發言的限制

( client.c 程式只有一個，但一個程式模擬兩個client )

執行畫面:

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/ryEurG8Sn.png" /></center>

<center>server</center>

<br>

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/BJptSzIr2.png" /></center>

<center>client</center>

<br>

server程式碼:
```c=
#include<stdio.h>
#include<winsock.h>
#include<stdlib.h>

void *thread_main(void *arg);

//thread的結構
struct Thread_Args
{
    int client_socket1;

    int client_socket2;
};

int main()
{
    //thread結構
    struct Thread_Args *thread_arg;

    //thread id
    DWORD thread_id;

    //set WSA and initial WSAStartup
	WSADATA wsadata;

	struct sockaddr_in server_addr;

	struct sockaddr_in client_addr;

    int serv_sock , clnt_sock1 , clnt_sock2 ;

	int i , n , client_len;

    //呼叫WSAStartup()註冊WinSock DLL的使用
	if( WSAStartup( 0x101 , &wsadata ) != 0 )
    {
        printf( "WSAStartup() failed" );

        exit(1);
    }

	//使用TCP協定
	if( ( serv_sock = socket( PF_INET , SOCK_STREAM , IPPROTO_TCP) ) < 0 )
    {
        printf( "socket() failed" );
    }

    //初始化server
    memset( &server_addr , 0 , sizeof(server_addr) );

    //設定server的結構
	server_addr.sin_addr.s_addr = htonl( INADDR_ANY );
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons( 5678 );

    //綁定server結構裡設定的資訊
	if( (bind( serv_sock , (struct sockaddr*) &server_addr , sizeof(server_addr) )) < 0 )
    {
        printf( "bind() failed" );
    }

    if( listen( serv_sock , 3 ) < 0 )
    {
        printf( "listen() failed" );
    }

    printf( "server is waiting for clients...\n" );

    while(1)
    {
        client_len = sizeof( client_addr );

        //處理client 1的連接
        if( ( clnt_sock1 = accept( serv_sock , (struct sockaddr*) &client_addr , &client_len ) ) <= 0 )
        {
            printf( "accept() failed" );
        }

        //處理client 2的連接
        if( ( clnt_sock2 = accept( serv_sock , (struct sockaddr*) &client_addr , &client_len ) ) <= 0 )
        {
            printf( "accept() failed" );
        }

        //配置記憶體給thread結構
	    thread_arg = (struct Thread_Args *) malloc( sizeof(struct Thread_Args) );

        //把client1的socket放入thread結構裡面
        thread_arg -> client_socket1 = clnt_sock1;

        //把client2的socket放入thread結構裡面
        thread_arg -> client_socket2 = clnt_sock2;

        //建立一個thread來服務兩個client
        if( CreateThread( NULL , 0 , (LPTHREAD_START_ROUTINE) thread_main , thread_arg , 0 , (LPDWORD) &thread_id ) == NULL )
        {
            printf( "CreateThread() failed" );
        }

        printf( "New chat-room with thread id: %1d and socket1: %d or socket2: %d\n" , thread_id , clnt_sock1 , clnt_sock2 );
    }

    //關閉TCP socket
	closesocket( serv_sock );

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}

void *thread_main( void *thread_arg )
{
    int client_sock1 , client_sock2 , recv_msg_size;

    char buffer[1000];

    int nerror;

    //紀錄該client1的socket
    client_sock1 = ( (struct Thread_Args *) thread_arg ) -> client_socket1;

    //紀錄該client2的socket
    client_sock2 = ( (struct Thread_Args *) thread_arg ) -> client_socket2;

    //當imode為1的時候，設定為非攔阻模式
    u_long imode = 1;
    //設定client1的socket為為非攔阻模式
    ioctlsocket( client_sock1 , FIONBIO , &imode );
    //設定client2的socket為為非攔阻模式
    ioctlsocket( client_sock2 , FIONBIO , &imode );

    while(1)
    {
        //接收client1的資訊
        if( ( recv_msg_size = recv( client_sock1 , buffer , 1000 , 0 ) ) > 0 )
        {
            //傳送資訊給client2
            send( client_sock2 , buffer , strlen(buffer)+1 , 0 );
        }

        nerror = WSAGetLastError();

        //如果client1中斷連線或是離開，就給出訊息並且結束
        if( nerror != WSAEWOULDBLOCK && nerror != 0 )
        {
            printf( "Disconnected! error code: %d\n" , nerror );

            break;
        }

        //接收client2的資訊
        if( ( recv_msg_size = recv( client_sock2 , buffer , 1000 , 0 ) ) > 0 )
        {
            //傳送資訊給client1
            send( client_sock1 , buffer , strlen(buffer)+1 , 0 );
        }

        nerror = WSAGetLastError();

        //如果client2中斷連線或是離開，就給出訊息並且結束
        if( nerror != WSAEWOULDBLOCK && nerror != 0 )
        {
            printf( "Disconnected! error code: %d\n" , nerror );

            break;
        }

        //緩衝時間1秒
        Sleep( 1000 );
    }

    //關閉client1的socket
    closesocket( client_sock1 );

    //關閉client2的socket
    closesocket( client_sock2 );

    //釋放該thread的結構
    free( thread_arg );

    return( NULL );
}
```

client程式碼:
```c=
#include<stdio.h>
#include<string.h>
#include<winsock.h>

#define MAXLINE 1024

int main()
{
    //set WSA and initial WSAStartup
	SOCKET sd1 , sd2;
	WSADATA wsadata;

	struct sockaddr_in serv;

	int i , n;

	char str1[100] = "I love NP!";

	char str2[100] = "I loven Algo!";

	//呼叫WSAStartup()註冊WinSock DLL的使用
	WSAStartup( 0x101 , (LPWSADATA) &wsadata );

	//使用TCP協定
	sd1 = socket( AF_INET , SOCK_STREAM , 0 );

	//使用TCP協定
	sd2 = socket( AF_INET , SOCK_STREAM , 0 );

	//設定server的結構
	serv.sin_addr.s_addr = inet_addr( "127.0.0.1" );
	serv.sin_family = AF_INET;
	serv.sin_port = htons( 5678 );

	//連接至server
	connect( sd1 , (LPSOCKADDR) &serv , sizeof(serv) );

	connect( sd2 , (LPSOCKADDR) &serv , sizeof(serv) );

    while(1)
    {
        //client1傳送訊息給server
        n = send( sd1 , str1 , strlen(str1)+1 , 0 );

        printf( "send: %s\n" , str1 );

        if( n < 0 )
        {
            break;
        }

        //緩衝時間1秒
        Sleep( 1000 );

        //接收client2的訊息
        n = recv( sd2 , str1 , MAXLINE , 0 );

        if( n <= 0 )
        {
            break;
        }

        printf( "recv: %s\n" , str1 );

        //client2傳送訊息給server
        n = send( sd2 , str2 , strlen(str2)+1 , 0 );

        printf( "send: %s\n" , str2 );

        if( n < 0 )
        {
            break;
        }

        //緩衝時間1秒
        Sleep( 1000 );

        //接收client1的訊息
        n = recv( sd1 , str2 , MAXLINE , 0 );

        if( n <= 0 )
        {
            break;
        }

        printf( "recv: %s\n" , str2 );
    }

    //關閉TCP socket
	closesocket(sd1);
	closesocket(sd2);

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}
```

## 進階練習3. Client多工

作業進階練習3是要去改聊天室的client，要讓client利用thread來達到多工的功能，也就是讓client可以輸入文字的同時也可以顯示聊天室的文字內容，server是用進階練習2的server，而client的main主要會去負責處理文字的輸入與傳送，然後client會使用一個thread，thread主要是負責處理顯示聊天室裡的訊息，這樣子client就可以顯示聊天室中的訊息也可以輸入文字到聊天室裡面

( client.c 程式只有一個，可以開多次來模擬多個client )

執行畫面:

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/HJL7qzLr2.png" /></center>

<center>server</center>

<br>

<center><img style="margin-top:auto;border:1px #eee;width:100%;" src="https://hackmd.io/_uploads/BJHVcGUB2.png" /></center>

<center>client</center>

<br>

server程式碼:
```c=
#include<stdio.h>
#include<winsock.h>
#include<stdlib.h>

void *thread_main(void *arg);

//thread的結構
struct Thread_Args
{
    int client_socket1;

    int client_socket2;
};

int main()
{
    //thread結構
    struct Thread_Args *thread_arg;

    //thread id
    DWORD thread_id;

    //set WSA and initial WSAStartup
	WSADATA wsadata;

	struct sockaddr_in server_addr;

	struct sockaddr_in client_addr;

    int serv_sock , clnt_sock1 , clnt_sock2 ;

	int i , n , client_len;

    //呼叫WSAStartup()註冊WinSock DLL的使用
	if( WSAStartup( 0x101 , &wsadata ) != 0 )
    {
        printf( "WSAStartup() failed" );

        exit(1);
    }

	//使用TCP協定
	if( ( serv_sock = socket( PF_INET , SOCK_STREAM , IPPROTO_TCP) ) < 0 )
    {
        printf( "socket() failed" );
    }

    //初始化server
    memset( &server_addr , 0 , sizeof(server_addr) );

    //設定server的結構
	server_addr.sin_addr.s_addr = htonl( INADDR_ANY );
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons( 5678 );

    //綁定server結構裡設定的資訊
	if( (bind( serv_sock , (struct sockaddr*) &server_addr , sizeof(server_addr) )) < 0 )
    {
        printf( "bind() failed" );
    }

    if( listen( serv_sock , 3 ) < 0 )
    {
        printf( "listen() failed" );
    }

    printf( "server is waiting for clients...\n" );

    while(1)
    {
        client_len = sizeof( client_addr );

        //處理client 1的連接
        if( ( clnt_sock1 = accept( serv_sock , (struct sockaddr*) &client_addr , &client_len ) ) <= 0 )
        {
            printf( "accept() failed" );
        }

        //處理client 2的連接
        if( ( clnt_sock2 = accept( serv_sock , (struct sockaddr*) &client_addr , &client_len ) ) <= 0 )
        {
            printf( "accept() failed" );
        }

        //配置記憶體給thread結構
	    thread_arg = (struct Thread_Args *) malloc( sizeof(struct Thread_Args) );

        //把client1的socket放入thread結構裡面
        thread_arg -> client_socket1 = clnt_sock1;

        //把client2的socket放入thread結構裡面
        thread_arg -> client_socket2 = clnt_sock2;

        //建立一個thread來服務兩個client
        if( CreateThread( NULL , 0 , (LPTHREAD_START_ROUTINE) thread_main , thread_arg , 0 , (LPDWORD) &thread_id ) == NULL )
        {
            printf( "CreateThread() failed" );
        }

        printf( "New chat-room with thread id: %1d and socket1: %d or socket2: %d\n" , thread_id , clnt_sock1 , clnt_sock2 );
    }

    //關閉TCP socket
	closesocket( serv_sock );

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}

void *thread_main( void *thread_arg )
{
    int client_sock1 , client_sock2 , recv_msg_size;

    char buffer[1000];

    int nerror;

    //紀錄該client1的socket
    client_sock1 = ( (struct Thread_Args *) thread_arg ) -> client_socket1;

    //紀錄該client2的socket
    client_sock2 = ( (struct Thread_Args *) thread_arg ) -> client_socket2;

    //當imode為1的時候，設定為非攔阻模式
    u_long imode = 1;
    //設定client1的socket為為非攔阻模式
    ioctlsocket( client_sock1 , FIONBIO , &imode );
    //設定client2的socket為為非攔阻模式
    ioctlsocket( client_sock2 , FIONBIO , &imode );

    while(1)
    {
        //接收client1的資訊
        if( ( recv_msg_size = recv( client_sock1 , buffer , 1000 , 0 ) ) > 0 )
        {
            //傳送資訊給client2
            send( client_sock2 , buffer , strlen(buffer)+1 , 0 );
        }

        nerror = WSAGetLastError();

        //如果client1中斷連線或是離開，就給出訊息並且結束
        if( nerror != WSAEWOULDBLOCK && nerror != 0 )
        {
            printf( "Disconnected! error code: %d\n" , nerror );

            break;
        }

        //接收client2的資訊
        if( ( recv_msg_size = recv( client_sock2 , buffer , 1000 , 0 ) ) > 0 )
        {
            //傳送資訊給client1
            send( client_sock1 , buffer , strlen(buffer)+1 , 0 );
        }

        nerror = WSAGetLastError();

        //如果client2中斷連線或是離開，就給出訊息並且結束
        if( nerror != WSAEWOULDBLOCK && nerror != 0 )
        {
            printf( "Disconnected! error code: %d\n" , nerror );

            break;
        }

        //緩衝時間1秒
        Sleep( 1000 );
    }

    //關閉client1的socket
    closesocket( client_sock1 );

    //關閉client2的socket
    closesocket( client_sock2 );

    //釋放該thread的結構
    free( thread_arg );

    return( NULL );
}
```

client程式碼:
```c=
#include<stdio.h>
#include<string.h>
#include<winsock.h>

#define MAXLINE 1024

void *thread_main(void *arg);

//thread的結構
struct Thread_Args
{
   SOCKET thread_sd;
};

int main()
{
    //thread結構
    struct Thread_Args *thread_arg;

    //thread id
    DWORD thread_id;

    //set WSA and initial WSAStartup
	SOCKET sd;
	WSADATA wsadata;

	struct sockaddr_in serv;

	int i , n;

	char str[1024];

	//呼叫WSAStartup()註冊WinSock DLL的使用
	WSAStartup( 0x101 , (LPWSADATA) &wsadata );

	//使用TCP協定
	sd = socket( AF_INET , SOCK_STREAM , 0 );

	//配置記憶體給thread結構
    thread_arg = (struct Thread_Args *) malloc( sizeof(struct Thread_Args) );

    //把socket放入thread結構裡面的socket
    thread_arg -> thread_sd = sd;

	//設定server的結構
	serv.sin_addr.s_addr = inet_addr( "127.0.0.1" );
	serv.sin_family = AF_INET;
	serv.sin_port = htons( 5678 );

	//連接至server
	connect( sd , (LPSOCKADDR) &serv , sizeof(serv) );

    //建立一個thread來服務兩個client
    if( CreateThread( NULL , 0 , (LPTHREAD_START_ROUTINE) thread_main , thread_arg , 0 , (LPDWORD) &thread_id ) == NULL )
    {
        printf( "CreateThread() failed" );
    }

    //負責文字的輸入與傳送
    while(1)
    {
        gets( str );

        //client傳送訊息給server
        n = send( sd , str , strlen(str)+1 , 0 );

        printf( "send: %s\n" , str );

        if( n < 0 )
        {
            printf( "send error...\n" );
        }
    }

    //關閉TCP socket
	closesocket(sd);

	//結束WinSock DLL的使用
	WSACleanup();

	system("pause");
}

//負責顯示聊天室的訊息
void *thread_main( void *thread_arg )
{
     SOCKET thread_sock;

     char str[MAXLINE];

     int n;

     //紀錄client的socket
     thread_sock = ( (struct Thread_Args *) thread_arg ) -> thread_sd;

     while(1)
     {
         //接收聊天室的訊息
         n = recv( thread_sock , str , MAXLINE , 0 );

         if( n <= 0 )
         {
             break;
         }

         printf( "recv: %s\n" , str );
     }

     //關閉TCP socket
	 closesocket(thread_sock);

	 //結束WinSock DLL的使用
	 WSACleanup();

	 return NULL;
}
```

## 本週心得
這次老師教的課程為thread，其實我很早就想要了解與學習thread的應用了，這次終於有機會可以好好的學習有關於thread的使用與講解，原來thread的應用有很多，就好像可以指派很多人去幫忙並且一起解決事情，分工合作的概念，這樣可以讓速度與效能提升不少，而且之前寫的聊天室作業非常陽春，輸入與輸入的程式還要分兩個執行，這樣就感覺很麻煩，但多虧了thread，終於寫出比較像樣的聊天室程式了，而且thread還可以搭配之前老師教的非阻攔模式與select()一起使用，這樣說不定就會有更好的功能與效能也說不定，但是thread也有一些缺點，也就是程式上需要寫或是定義比較多東西，也要去避免資料可能會有覆蓋的情況發生，所以在寫thread的時候要特別的小心去處理這些資料的傳遞與資料被修改問題，這次作業只有3個，我寫的比較輕鬆也非常開心，這樣壓力比較小一點，助教也很開心，因為改一位同學只要看3個程式題目就好，話說助教大哥HW08和HW09的成績記得給老師，感謝助教大哥改作業那麼辛苦，也感謝老師提供教學的影片，終於學到thread的應用與課程，非常期待老師下禮拜的課程與內容，感謝老師與助教大哥

### 作者: 廖經翰