• Thứ Năm, 15/10/2009 09:18 (GMT+7)

    Lập trình gửi nhận dữ liệu trong Linux

    Câu hỏi :
    Xin hướng dẫn cách viết chương trình C trên Linux, tạo 2 process producer và customer. Producer tạo ra dữ liệu ngẫu nhiên gởi cho process customer. Customer nhận và xử lí.


    Trả lời :

    Để lập trình giải quyết vấn đề nào đó, bạn cần tìm hiểu và nắm vững giải thuật giải quyết vấn đề đó. Cụ thể bài toán Sản xuất-Tiêu dùng (Producer/Consumer) là 1 trong số ít bài toán kinh điển giới thiệu vấn đề cần phải đồng bộ hóa giữa các process và phải loại trừ tương hỗ giữa các process khi chúng đồng thời cùng truy xuất vào tài nguyên dùng chung nào đó. Giải thuật giải quyết bài toán Sản xuất-Tiêu dùng dùng kỹ thuật gửi/nhận thông báo đã được trình bày trong các sách giáo trình môn Hệ điều hành. Đây là kỹ thuật rất an toàn và tổng quát, vừa áp dụng được cho các process chạy trên cùng máy hay cho các process chạy trên các máy khác nhau. Việc hiện thực nó không phụ thuộc vào ngôn ngữ và vào HĐH. Sau đây chúng tôi giới thiệu qui trình điển hình để xây dựng 2 ứng dụng hợp thành bài toán Sản xuất-Tiêu dùng bằng ngôn ngữ VC++ của Microsoft trên Windows dùng socket để giao tiếp nhau thông qua môi trường mạng Internet. Qui trình gồm các bước sau:

     1. Chạy VC++ 6.0, chọn menu File.New để hiển thị cửa sổ New, chọn tab "Projects", chọn mục "Win32 Console Application", chọn vị trí "Location" chứa Project trên đĩa, nhập tên Project chứa ứng dụng Sản xuất (thí dụ Producer), chọn button OK. Khi cửa sổ Step 1 hiển thị, chọn checkbox "An empty project" rồi button Finish để tạo project trống ban đầu.

     2. Chọn menu File.New để hiển thị cửa sổ New, chọn tab "Files", chọn mục "C++ Source File", nhập tên file mã nguồn (producer) rồi nhấn button OK để tạo file mã nguồn trống ban đầu của ứng dụng.

     3. Khi cửa sổ mã nguồn trống ban đầu của file hiển thị, bạn nhập đoạn code cấu thành ứng dụng Producer như sau:
     //khai báo các thư viện cần dùng
     #include <winsock2.h>
     #include <stdio.h>
     #include <string.h>
     #include <conio.h>
     #include <winbase.h>
     
     //khai báo các biến toàn cục cần dùng
     int timesx, idx = 0;
     char sItem[256], sRequest[256];
     
     //hàm thực hiện việc sản xuất 1 sản phẩm
     void Produce_Item(char* Item) {
     sprintf(Item, "Sản phẩm thứ %d.\n",idx++);
     //giả lập thời gian sản xuất sản phẩm để dễ quan sát
     Sleep(timesx);
     }
     
     //hàm thực hiện việc thông báo chứa sản phẩm
     void Buid_Message(char* Item) {
     //tùy định dạng thông báo mà bạn viết lấy
     //ở đây chúng tôi không viết và xem thông báo chính là sản phẩm
     }
     
     //điểm nhập chương trình
     int main(int argc, char **argv) {
     //khai báo các biến cần dùng
     int retval, fromlen;
     SOCKADDR_IN local, from;
     WSADATA wsaData;
     SOCKET listen_socket, msgsock;
     if (argc <2) {
     printf("Nên nhập lệnh Producer <timesx>\\n\n");
     timesx = 1;
     } else timesx = atoi(argv[1]);
     printf ("Lưu ý : sản xuất mới sản phẩm tốn %d ms\n",timesx);
     
     //khởi động dịch vụ socket
     if (WSAStartup(0x202,&wsaData) == SOCKET_ERROR) {
     fprintf(stderr,"WSAStartup bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //thiết lập các giá trị cấu hình server
     local.sin_family = AF_INET;
     local.sin_addr.s_addr = INADDR_ANY;
     local.sin_port = htons(2048);
     //tạo socket lắng nghe của server
     listen_socket = socket(AF_INET, SOCK_STREAM,0); //TCP socket
     if (listen_socket == INVALID_SOCKET){
     fprintf(stderr,"socket() bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     if (bind(listen_socket,(LPSOCKADDR)&local,sizeof(local)) == SOCKET_ERROR) {
     fprintf(stderr,"bind() bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //khai báo số lượng client maximum
     if (listen(listen_socket,SOMAXCONN) == SOCKET_ERROR) {
     fprintf(stderr,"listen() bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //chờ yêu cầu kết nối từ consumer
     printf("Chờ yêu cầu kết nối từ consumer...\n");
     fromlen=sizeof(from);
     msgsock = accept(listen_socket,(struct sockaddr*)&from, &fromlen);
     if (msgsock == INVALID_SOCKET) {
     fprintf(stderr,"accept() bị lỗi : error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //thiết lập chỉ số sản phẩm đầu tiên
     idx = 0;
     //lặp sản xuất theo nhịp điệu của client
     //nếu muốn ngừng thì user phải gỏ 1 phím nào đó
     while(!_kbhit()) {
     //sản xuất 1 sản phẩm mới
     Produce_Item(sItem);
     //chờ nhận yêu cầu sản xuất từ consumer
     printf("Chờ nhận yêu cầu sản xuất từ consumer...\n");
     retval = recv(msgsock,sRequest,256,0 );
     if (retval == SOCKET_ERROR) {
     fprintf(stderr,"recv() bị lỗi : error %d\n",WSAGetLastError());
     closesocket(msgsock);
     return -1;
     }
     //xây dựng thông báo chứa sản phẩm
     Buid_Message(sItem);
     //gởi thông báo chứa sản phẩm cho consumer dùng
     printf("Gởi thông báo chứa sản phẩm cho consumer dùng...\n\n");
     send(msgsock,sItem,sizeof(sItem),0);
     }
     closesocket(msgsock);
     return 0;
     }
     4. Chọn menu Build.Set Active Configuration để hiển thị cửa sổ cấu hình, chọn mục "Win32 Release" rồi button OK để qui định máy dịch ra file khả thi ở chế độ phân phối (không có thông tin debug bên trong hầu giảm kích thước file khả thi).

     5. Build.Rebuild All để dịch ứng dụng ra file khả thi. Với cấu hình như bước 4 thì file khả thi có tên là Producer.exe nằm trong thư mục con Release của Project.

     6. tương tự cho việc xây dựng Consumer. Chạy VC++ 6.0, chọn menu File.New để hiển thị cửa sổ New, chọn tab "Projects", chọn mục "Win32 Console Application", chọn vị trí "Location" chứa Project trên đĩa, nhập tên Project chứa ứng dụng Tiêu dùng (thí dụ Consumer), chọn button OK. Khi cửa sổ Step 1 hiển thị, chọn checkbox "An empty project" rồi button Finish để tạo project trống ban đầu.

     7. chọn menu File.New để hiển thị cửa sổ New, chọn tab "Files", chọn mục "C++ Source File", nhập tên file mã nguồn (consumer) rồi nhấn button OK để tạo file mã nguồn trống ban đầu của ứng dụng.

     8. Khi cửa sổ mã nguồn trống ban đầu của file hiển thị, bạn nhập đoạn code cấu thành ứng dụng Consumer như sau:
     //khai báo các thư viện cần dùng
     #include <winsock2.h>
     #include <stdio.h>
     #include <string.h>
     #include <conio.h>
     #include <winbase.h>
     //khai báo các biến toàn cục cần dùng
     const N = 5; //dung lượng kho chứa sản phẩm
     int timetd;
     char sItem[256], sRequest[256];
     
     //hàm thực hiện việc tiêu dùng 1 sản phẩm
     void Consume_Item(char* Item) {
     //giả lập thời gian tiêu dùng sản phẩm để dễ quan sát
     Sleep(timetd);
     printf("Nội dung sản phẩm nhận được : %s\n", Item);
     }
     //hàm rút trích sản phẩm từ thông báo
     void Extract_Item(char* Item) {
     //tùy định dạng thông báo mà bạn viết lấy
     //ở đây chúng tôi không viết và xem thông báo chính là sản phẩm
     }
     
     //điểm nhập chương trình
     int main(int argc, char **argv) {
     //khai báo các biến cần dùng
     int i, retval;
     SOCKADDR_IN ser_addr;
     WSADATA wsaData;
     SOCKET sock;
     if (argc <2) {
     printf("Nên nhập lệnh Consumer <timetd>\\n\n");
     timetd = 1;
     } else timetd = atoi(argv[1]);
     printf ("Lưu ý : sản xuất mới sản phẩm tốn %d ms\n",timetd);
     
     //khởi động dịch vụ socket
     if (WSAStartup(0x202,&wsaData) == SOCKET_ERROR) {
     fprintf(stderr,"WSAStartup bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //tạo socket giao tiếp, nếu thất bại báo lỗi
     sock=socket(AF_INET,SOCK_STREAM,0);
     if (sock==INVALID_SOCKET) {
     fprintf(stderr,"socket() bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     // thiết lập địa chỉ giao tiếp của server
     ser_addr.sin_family=AF_INET;
     ser_addr.sin_port=htons(2048);
     ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
     //yêu cầu nối kết tới server
     if (connect(sock,(LPSOCKADDR)&ser_addr,sizeof(ser_addr))==SOCKET_ERROR) {
     fprintf(stderr,"connect() bị lỗi : Error %d\n",WSAGetLastError());
     WSACleanup();
     return -1;
     }
     
     //gởi N yêu cầu sản xuất (vì kho chứa có N chỗ)
     sprintf(sRequest,"Hay san xuat them san pham moi\n");
     for (i = 0 ; i < N ; i++)
     send(sock,sRequest,sizeof(sRequest),0);
     //lặp tiêu dùng từng sản phẩm
     //nếu muốn ngừng thì user phải gỏ 1 phím nào đó
     while(!_kbhit()) {
     //chờ nhận thông báo chứa sản phẩm
     printf("Chờ nhận thông báo chứa sản phẩm từ Producer...\n");
     retval = recv(sock,sItem,256,0 );
     if (retval == SOCKET_ERROR) {
     fprintf(stderr,"recv() bị lỗi : error %d\n",WSAGetLastError());
     closesocket(sock);
     return -1;
     }
     //rút trích sản phẩm từ thông báo
     Extract_Item(sItem);
     //gởi thông báo yêu cầu sản xuất tiếp
     printf("Gởi thông báo yêu cầu sản xuất tiếp...\n");
     send(sock,sRequest,sizeof(sRequest),0);
     //tiêu dùng sản phẩm vừa nhận
     Consume_Item(sItem);
     }
     closesocket(sock);
     exit(0);
     }
     9. Chọn menu Build.Set Active Configuration để hiển thị cửa sổ cấu hình, chọn mục "Win32 Release" rồi button OK để qui định máy dịch ra file khả thi ở chế độ phân phối (không có thông tin debug bên trong hầu giảm kích thước file khả thi).

     10. Build.Rebuild All để dịch ứng dụng ra file khả thi. Với cấu hình như bước 9 thì file khả thi có tên là Consumer.exe nằm trong thư mục con Release của Project.

    Sau khi có được 2 ứng dụng Sản xuất - Tiêu dùng, bạn có thể thử chạy chúng bằng cách copy 2 file khả thi tạo được vào cùng 1 thư mục, tạo 2 cửa sổ "Command Prompt" độc lập (nên hiệu chỉnh vị trí và kích thước của 2 cửa sổ này sao cho bạn có thể thấy đồng thời cả 2 cửa sổ). Trong cửa sổ 1, bạn dùng lệnh cd để chuyển về thư mục chứa 2 file khả thi rồi chạy ứng dụng Producer bằng cách nhập dòng lệnh "producer 1", còn trong cửa sổ 2, bạn cũng dùng lệnh cd để chuyển về thư mục chứa 2 file khả thi rồi chạy ứng dụng Consumer bằng cách nhập dòng lệnh "consumer 2000" rồi quan sát tiến độ chạy của 2 ứng dụng. Lưu ý tham số kèm theo dòng lệnh producer hay consumer là thời gian được tính bằng milisecond mà trình sản xuất/tiêu dùng sẽ tốn để sản xuất/tiêu dùng 1 sản phẩm. Bạn nên thử thay đổi giá trị này thành nhiều giá trị khác và kiểm nghiệm tốc độ làm việc của từng ứng dụng.

    Lưu ý trong đoạn code Producer trên, chúng tôi cho process Producer chạy trên cùng máy với Consumer và Producer giao tiếp ở địa chỉ TCP là "127.0.0.1:2048". Tùy theo nhu cầu, bạn có quyền thay đổi địa chỉ TCP của process Producer để nó có thể làm việc ở bất kỳ máy nào và port giao tiếp nào.
     

    Chuyên mục: Lập trình