Đồ án Lập trình đa tiến trình xây dựng chương trình chatting trên mạng Linux

MỤC LỤC

MỤC LỤC 7

TỔNG QUAN ĐỀ TÀI 9

I. HỆ ĐIỀU HÀNH LINUX 9

II. GIAO TIẾP GIỮA CÁC MÁY CÓ SỬ DỤNG HỆ ĐIỀU HÀNH LINUX 9

II.1.Giới thiệu một số tiện ích giao tiếp qua mạng Linux 10

a) Lệnh gọi hệ thống talk 10

b) Lệnh gọi hệ thống mail 10

III. GIỚI THIỆU NỘI DUNG ĐỀ TÀI 10

CƠ SỞ LÝ THUYẾT 12

I. NGÔN NGỮ C 12

I.1. Giới thiệu sơ lược 12

I.1.1. Lập trình C trên Linux 12

I.1.2. Một chương trình C đơn giản 12

I.1.3. Biên dịch và thực hiện chương trình 13

I.2.Quản lý đa tiến trình bằng ngôn ngữ C 14

I.2.1. Tiến trình và đa tiến trình 14

a) Khái niệm tiến trình 14

b) Trạng thái của các tiến trình 15

I.2.2. Các dấu hiệu nhận biết tiến trình 15

I.2.3. Tạo ra một tiến trình 16

I.2.4. Chấm dứt một tiến trình 16

I.3. Trao đổi thông tin giữa các tiến trình 17

I.3.1. Liên lạc bằng các tín hiệu 17

a) Khái niệm 17

b) Một số tín hiệu thường gặp 18

c) Liên lạc bằng các tín hiệu 18

d) Một số giới hạn khi sử dụng tín hiệu 20

I.3.2. Liên lạc bằng ống dẫn 20

a) Khái niệm 20

b) Các đặc điểm của ống dẫn 21

c) Khởi tạo và liên lạc bằng ống dẫn 21

I.3.3. Trao đổi thông tin giữa các tiến trình bằng hệ thống IPC 24

a) Khái niệm 24

b) Khởi tạo dãy thông báo 25

c) Gởi thông tin lên dãy thông báo 26

d) Nhận thông tin từ dãy thông báo 27

I.4. Giao tiếp mạng 29

I.4.1. Khái niệm socket và port 29

I.4.2. Tạo ra một socket 29

I.4.3. Chờ đợi một kết nối từ client(server) 31

I.4.4. Thực hiện kết nối đến server (client) 32

I.4.5. Ví dụ mẫu mô hình client/server 33

a) Mã lệnh chương trình server 33

b) Mã lệnh chương trình client 34

II. NGÔN NGỮ TCL/TK 36

II.1.Khái niệm 36

II.1.1. Khái niệm 36

II.1.2. Các thành phần cơ bản của tcl/tk 36

a) Cấu trúc lệnh 36

b) Biến và giá trị của biến 37

c) Các cấu trúc lặp 38

d) Chương trình con 39

II.2. Một chương trình đơn giản bằng tcl/tk 39

II.2.1. Cấu trúc chương trình 39

II.2.2. Thực hiện chương trình 40

II.3. Sử dụng socket trong tcl/tk 40

XÂY DỰNG MÔ HÌNH CLIENT/SERVER 42

I. XÂY DỰNG CHƯƠNG TRÌNH SEVER 42

I.1. Nguyên tắc hoạt động 42

I.2. Sơ đồ thuật toán thành phầnchờ đợi kết nối 43

I.2.1. Khởi tạo socket 43

I.2.2. Khởi tạo dãy thông báo 44

I.2.3. Đợi một kết nối từ client 45

I.3. Sơ đồ thuật toán thành phần xử lý thông tin trao đổi 46

I.3.1. Phân tích tham số đầu vào 47

I.3.2. Khởi tạo socket và tìm kiếm dãy thông báo 47

I.3.3. Đợi một kết nối từ client 47

I.3.4. Thực hiện trao đổi thông tin 47

I.4. Cấu trúc dữ liệu 47

I.5. Cổng giao tiếp với các client: 48

I.6. Quản lý trao đổi thông tin 49

II. CHƯƠNG TRÌNH CLIENT 50

II.1. Nguyên tắc hoạt động 50

II.2 Sơ đồ thuật toán 51

II.3 Thiết lập kết nối đến chương trình server 51

II.4 Phân loại thông tin nhận được từ máy chủ 52

CHƯƠNG IV 53

THỬ NGHIỆM CHƯƠNG TRÌNH 53

I. TRÌNH TỰ SỬ DỤNG CHƯƠNG TRÌNH 53

II. 53

III. KẾT QUẢ 53

KẾT LUẬN 54

I. KẾT QUẢ ĐẠT ĐƯỢC 54

I.1.Chương trình server 54

I.2. Chương trình client 54

I.3. Tính khả thi 54

II. HẠN CHẾ 54

III. HƯỚNG PHÁT TRIỂN ĐỀ TÀI 55

TÀI LIỆU THAM KHẢO 56

 

 

doc49 trang | Chia sẻ: netpro | Lượt xem: 3152 | Lượt tải: 2download
Bạn đang xem trước 20 trang tài liệu Đồ án Lập trình đa tiến trình xây dựng chương trình chatting trên mạng Linux, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
nh của tín hiệu. Tên hàm xử lý tín hiệu. Khi nhận một tín hiệu, hệ thóng ngắt quá trình thực hiện của tiến trình để thực hiện hàm xử lý tín hiện. Vì thế chúng ta thấy rằng có thể bổ sung hoạt động của một tiến trình khi có tín hiệu xuất hiện. Hay nói cách khác là chúng ta có thể thiết lập mối liên lạc giữa các tiến trình. Việc gởi một tín hiệu được thực hiện bằng hàm sau: #include int kill(pid_t pid, int sig); Đối số thứ hai chỉ ra một tín hiệu xác định sẽ được gởi. Đối số thứ nhất chỉ ra đối tượng sẽ nhận tín hiệu này. Nếu pid là một số dương thì tín hiệu sẽ được gởi đến cho tiến trình có dấu hiệu nhận dang là pid. Nếu pid là không thì tín hiệu sẽ được gởi đến cho tất cả các tiến trình có cùng nhóm với tiến trình phát tín hiệu. Nếu pid bằng -1 tín hiệu sẽ được gởi đến tất cả các tiến trình trừ tiến trình init (tiến trình gốc). Nếu pid nhỏ hơn -1 tín hiệu sẽ đueọec gởi đến các tiến trình thuộc nhóm có dấu hiệu nhận biết là giá trị tuyệt đối của pid. Nếu tín hiệu gởi đi là không thì không có một tín hiệu nào được gởi, hàm chỉ có tác dụng kiểm tra tiến trình nhận có tồn tại hay không. Hàm sẽ trả về giá trị -1 nếu gặp lỗi và giá trị 0 nếu hàm kết thúc tốt đẹp. Một số giới hạn khi sử dụng tín hiệu Trừ tín hiệu SIGCLD, tất cả các tín hiệu khi được một tiến trình nhận được đều không được ghi nhớ lại. Chúng có thể bị bỏ qua, kết thúc tiến trình hoặc bị chặn lại cho một hàm xử lý khác. Tuy nhiên mọi thao tác phải trong tức thời. Đó là lý do đưa ra để xác nhận rằng việc dùng các tín hiệu nhằm mục đích liên lạc giữa các tiến trình không được thích ứng tốt. Một thông điệp dưới dạng tín hiệu có thể bị bỏ qua nếu tiến trình nhận được lúc tín hiệu đang tạm thời bị bỏ qua. Mặt khác quyền của các tín hiệu là khá lớn, khi đến tín hiệu làm ngắt quảng các hoạt động hiện tại của tiến trình. Thông thường sẽ không có vấn đề xảy ra, nhưng nếu tiến trình đang chờ đợi một thao tác với các thiết bị, các tài nguyên thực hiện xong thì sẽ gây ra sự xáo trộn khi tiến trình xử lý xong tín hiệu và quay trở lại với công việc trức đó. Một điều không được mong muốn trong liên lạc. Liên lạc bằng ống dẫn Khái niệm Ống dẫn là một cơ chế cơ bản để liên lạc gián tiến giữa các tiến trình. Đó là các tập tin FIFO đặc biệt (vào trước ra trước), ở đó thông tìn được truyền vào một hướng và được lấy ra ở một hướng khác. Tiến trình A Tiến trình B Sơ đồ mô tả hoạt động của ống dẫn Tiến trình A và B phải có mối quan hệ họ hàng Các đặc điểm của ống dẫn Các ống dẫn liên lạc là không có tên nên chúng chỉ mang tính chất tạm thời. Chúng tồn tại trong thời gian thực hiện của tiến trình tạo ra chúng. Việc tạo ra ống dẫn phải dùng một hàm đặc biệt pipe (chúng ta sẽ xét đến phần sau). Nhiều tiến trình có thể cùng ghi đọc đồng thời trên một ống dẫn những lại không có cơ chế phân biệt các thông tin ở đầu ra, chúng là như nhau. Dung lượng ống dãn bị hạn chế (vào khoảng 4096 byte). Nếu ta tiếp tục ghi vào một ống dẫn đã đầy sẽ xảy ra hiện tượng tắc nghẽn thông tin trong ống. Các tiến trình liên lạc qua ống dẫn phải có mối quan hệ họ hàng (cha con . .con) và các ống dẫn phải được tạo ra trước khi tạo ra một tiến trình con. Không thể tự thay đổi vị trí các thông tin trong ống. Khởi tạo và liên lạc bằng ống dẫn Để khởi tạo một ống dẫn ta sử dụng hàm pipe có khai báo như sau: int p_inf[2]; int pipe(p_inf); Hàm trả về giá trị 0 không nếu khởi tạo thành công, nếu gặp lỗi trả về giá trị -1. Hàm pipe sử dụng một đối số là một bảng gồm hai thành phần chính là p_inf[0] và p_inf[1]. Hai thành phần này chứa bộ mô tả đầu vào hay đầu ra của thông tin cho các tiến trình sử dụng nó. p_inf[0] : Chứa bộ mô tả đầu ra của ống dẫn. p_inf[0] : Chứa bộ mô tả đầu vào của ống dẫn. Các ống dẫn được quản lý thống nhất với hệ thống tập tin. Việc truy xuất đến các ống dẫn được thực hiện bằng các bộ mô tả vào ra của ống dẫn. Các thao tác dùng để truy cập thông tin của ống dẫn được dùng bằng các hàm nhập xuất chuẫn như read, write, .. Sau khi tạo ra ống dẫn, sử dụng ống dẫn làm phương tiện truyền thông bằng cách tạo ra các tiến trình con. Khi một tiến trình con được tạo ra, nó được thừa kế cả những thông tin về các bộ mô tả vào ra của các ống dẫn mà tiến trình cha của nó đã tạo ra. Từ đó một tiến trình con có thể sử dụng ống dẫn để truyền thông với tiến trình đã tạo ra nó. Điều quan trọng là thực hiện những thaotác này theo thứ tự. Do đó để truyền thông một cách chính xác, các tiến trình nên chọn một hướng truyền trên ống dẫn, một tiến trình chỉ đọc và tiến trình kia chỉ ghi trên ống dẫn. Các bộ mô tả không sử dụng có thể được đóng lại bằng hàm chuẫn close(). Dưới đây là một ví dụ cho việc truyền thông giữa hai tiến trình cha và tiến trình con thông qua ống dẫn. #include #include void tt_con(int number) { int fd; int nread; char text[100]; fd=number; printf("Dau hieu mo ta la: %d\n", fd); switch (nread=read(fd, text, sizeof(text))) { case -1: printf("Loi khi doc trong ong dan."); case 0: printf("Ong dan rong."); default: printf(" Co %d ky tu trong ong la: %s \n", nread, text); } } /***********************************/ main() { int fd[2]; if (pipe(fd)== -1) printf("Loi khoi tao ong dan."); switch (fork()) { case -1: printf("Loi fork()."); case 0: tt_con( fd[0] ); } close(fd[0]); if (write(fd[1], "Hello", 6) == -1) printf("Loi ong dan."); } Kết quả của chương trình mẫu trên: Dau hieu mo ta la: 3 Co 6 ky tu tro ong là: Hello Qua chương trình ngắn trên chúng ta có thể hiểu được nguyên tắc tạo ra một ống dẫn, kết hợp ống dẫn với việc tạo ra một tiến trình con và thực hiện trao đổi thông tin giữa tiến trình cha vàtiến trình con. Ở đây chúng ta chỉ mới xem xét theo một hướng, thông tin truyền từ tiến trình cha đến tiến trình con, ngoài ra chúng ta có thể truyền theo hai hướng trong ống dẫn đã tạo ra bằng cách đọc trong p_inf[0] và ghi trong p_inf[1]. Để truyền được theo hai huớng độc lập thì việc ghi và đọc của hai tiến trình phải diễn ra tuần tự. Ví dụ trên bắt đầu tạo một ống dẫn bằng hàm pipe( p_inf ). Sau đó chương trình tiếp tục tạo ra một tiến trình con bằng hàm fork(). Lúc này chúng ta đã có hai tiến trình cha con và một ống dẫn mà cả hai tiến trình đều có quyền truy cập. Tiến trình con sau khi được tạo ra sẽ gọi hàm tt_con( int ), truyền cho hàm bộ mô tả đọc của ống dẫn. Hàm tt_con(int) có nhiệm vụ đọc thông ống dẫn và in nội dung. Tiến trình cha sau khi tạo ra tiến trình con sẽ ghi vào ống dẫn một đoạn thông báo và chờ đợi tiến trình con kết thúc. So với việc liên lạc giữa các tiến trình bằng tín hiệu, các tiến trình đã có thể sử dụng các ống dẫn để truyền các thông tin cho nhau một các dể dàng hơn. Nhưng đối với một hệ thống bao gồm nhiều tiến trình hoạt động độc lập thì tình phức tạp của các ống dẫn sẽ không đáp ứng được. Mặt khác các ống dẫn có giới hạn về dung lượng thông tin và chỉ những tiến trình có quan hệ "họ hàng" mới có quyền truy cập đến các ống dẫn. Trao đổi thông tin giữa các tiến trình bằng hệ thống IPC IPC: Inter Process Comunication (truyền thông đa tiến trình). Khái niệm Các IPC không chỉ cho phép trao đổi và dùng chung dữ liệu mà còn cho phép đồng bộ các tiến trình. Các IPC bắt đầu được sử dụng từ phiên bản System V của Unix. Ngày nay, các IPC đã được sử dụng trên tất cả các hệ biến đổi của Unix. Các IPC cho phép xử lý tất cả các kiểu dữ liệu, gởi dự liệu từ tiến trình này đến tiến trình khác hoặc cho phép dùng chung dữ liệu. Trong thực tế, các IPC gồm có ba cơ chế: + Các dãy thông báo: Một dãy thông báo có thể được xem như là một hộp thư. Điều này có nghĩa là nếu có quyền truy cập cần thiết, một trình ứng dụng có thể gởi một thông báo đến đó (số, một xâu chuỗi, hay thậm chí cả nội dung của một cấu trúc dữ liệu, ...) và các trình ứng dụng khác có thể đọc thông báo này. + Bộ nhớ dùng chung: Việc quản lý bộ nhớ dùng chung cho phép nhiều trình ứng dụng sử dụng chung một vùng bộ nhớ. Thông thường khi một vùng bộ nhớ được cấp phát thì nó thuộc vào một tiến trình và các tiến trình khác đang làm việc trên hệ thống không thể truy cập đến vùng nhớ này. Việc quản lý bộ nhớ dùng chung cho phép nhiều tiến trình có thể truy cập đên svùng nhớ để đọc hay ghi lê đó. + Các dấu hiệu (semaphore): Các dấu hiệu giúp khắc một trong những khó khăn lớn nhất đối với một hệ thống đa tiến trình: đó là sự động bộ hóa tiến trình. Thật vậy, nhiều tiến trình cùng hoạt động vào một thời điểm và có thể truy cập cùng một dữ liệu. Điều này có thể gây ra những khó khăn nhất định như truy cập đồng thời vào dữ liệu, gây tắc nghẽn ... Trong đồ án này chúng ta chỉ xét cách sử dụng các dãy thông báo để truyền thông giữa các tiến trình. Một đặc trưng của các IPC là mặc dù có sử dụng các tập tin nhưng lại không sử dụng hệ thống quản lý tệp. Khi một IPC được tạo ra hay xử lý nó không được sử dụng giống như một tập tin, do đó không sử dụng các hàm vào ra chuẫn như read, open, ... Các IPC dựa vào một cơ chế quản lý đặc biệt là quản lý khóa. Khóa là một số nhận dạng cho một IPC ở mức độ hệ thống. Do đó để tạo ra hay chỉ để truy cập một IPC cũng cần phải biết đến khóa của IPC đó. Khởi tạo dãy thông báo Như đã nêu trong phần trên, các IPC được quản lý dựa vào một cơ chế đặc biệt là quả lý khóa. Do đó trước tiên chúng ta phải tạo ra khóa cho mỗi IPC trước khi sử dụng đến nó. #include key_t ftok(char *pathname, char prg); Hàm trả về một nhận dạng cho IPC được tạo ra từ file trong đối số thứ nhất. Hàm trên dựa vào tổ hợp các nhận dạng file như số inode file, số thiết bị tạo ra file, kết hợp với tham số thứ hai là một ký tự để tạo ra một khóa độc nhất cho IPC. Sau khi tạo khóa cho IPC, từ khóa nhận dạng vừa có chúng ta còn định nghĩa một dãy thông báo trước khi sử dụng chúng. Các dãy thông báo thường được so sánh với hệ thống các hộp thư. Điều này có nghĩa là một tiến trình bất kỳ có quyền truy cập dãy thông báo có thể gởi lên dãy thông báo các thông tin của mình cần tryền và các tiến trình khác sẽ truy cập đến dãy thông báo để nhận lấy thông tin có kiểu mà tiến trình đó cần theo thứ tự đến của thông báo. Hàm khởi tạo hoặc tìm kiếm một dãy thông báo được định nghĩa như sau: #include #include #include int msgget(key_t key, int option); Đối số thứ nhất là khóa của dãy thông báo đã tồn tại hay sắp được tạo ra. Đối số thứ hai là sự lựa chọn giữa các hằng trong bảng sau: TÊN HẰNG GIÁ TRỊ IPC_CREAT 01000 IPC_EXCL 02000 Nếu đối số thứ hai có giá trị là IPC_CREAT thì dãy thông báo sẽ được tạo ra. Nếu đối số có giá trị khác sẽ có hai khả năng được thực hiện: Nếu khóa chưa được sử dụng bởi dãy thông báo nào thì một dãy thông báo sẽ được tạo ra cùng với khóa được truyền. IPC_CREAT được xem là ngầm định. Nếu khóa đang được sử dụng bởi một dãy thông báo khác. Lúc này hoặc IPC_CREAT hoặc IPC_EXCL được dùng làm tham số và tiến trình có thể sử dụng dãy thông báo đã có để ghi, đọc các thông báo trên dãy. Hàm sẽ trả về dấu hiệu nhận dạng dãy thông báo để tiến trình sử dụng khi cần gởi thông tin lên dãy thông báo hay tìm kiếm thông tin trên đó. Gởi thông tin lên dãy thông báo Gởi thông tin lên dãy thông báo hay gọi là gởi một thông báo lên dãy. Để gởi thông báo lên dãy, chúng ta phải có bộ nhận dạng dãy thông báo do chúng ta khởi tạo hay sử dụng một dãy thông báo đã có sẵn từ trước. Hàm msgsnd() sau đây cho phép gởi một thông báo đến dãy thông báo: #include #include #include int msgsnd(int msgid, struct msgbuf *msgp, int msgsize, int msgopt); Đối số thứ nhất là nhận dạng của dãy thông báo cần gởi đến. Đối số tiếp theo chính là thông báo cần gởi. trong cấu trúc của thông báo gồm có hai thành phần như sau: struct msgbuf { long type; /*Kiểu thông báo*/ char *msgtext; /*Các nội dung thông báo*/ }; Thông thường chúng ta không sử dụng hoàn toàn nguyên mẫu theo cấu trúc này để truyền các thông tin lên dãy thông báo. Tuy nhiên thành phần đầu tiên của cấu trúc thông báo được truyền phải là một trường kiểu long và tiếp sau đó là các nội dung của thông báo. Chính trường kiểu long sẽ quy định kiểu của thông báo trên dãy, các tiến trình sẽ căn cứ vào đây để lựa chọn các thông báo cần thiết. Nếu tùy chọn IPC_NOWAIT được truyền cho đối số msgopt thì khi dãy thông báo bị đầy hàm vẫn được thực hiện nhưng lỗi EAGAIN được trả về. Sau đây là danh sách các lỗi có thể gặp phải khi gởi thông báo: Lỗi Ý nghĩa EAGAIN Không thể gởi thông báo do dãy đã đầy và IPC_WAIT đã xác lập EACCES Không có quyền ghi lên dãy thông báo EFAULT Địa chỉ do msgp chỉ đến không truy cập được EIDRM Dãy không còn nữa EINTR Tiến trình nhận một tín hiệu làm ngắt quá trình gởi EINVAL Nhận dạng dãy không đúng ENOMEM Không đủ bộ nhớ. Nhận thông tin từ dãy thông báo Hàm cho phép nhận một thông báo từ dãy có kiểu xác định: #include #include #include int msgrcv(int msgid, struct msgbuf *msgp, int msgsize, long type, int msgflg); Thông báo nhân được chứa trong nội dung bộ nhớ do msgp trỏ đến. Vùng nhớ có cở lớn nhất là msgsize . Hàm này chỉ nhận lấy thông báo có kiểu đước xác định bởi type. type = 0: Thông báo thứ nhất của dãy (củ nhất) cho dù thuộc loại nào sẽ được đọc ra. type < 0: Thông báo đầu tiêu có kiểu nhỏ hơn hoặc bằng trị tuyệt đốicủa type sẽ được đọc. type > 0: Thông báo đầu tiên có kiểu chính xác bằng type được đọc ra. Trường msgflg có hai giá trị: MSG_NOERROR: Nếu kích cở thông báo lớn hơn độ rông do trường msgsize quy định thì thông báo sẽ bị cắt bớt, phần bị cắt sẽ mất đi. Nếu không thông báo sẽ không được đưa ra khỏi dãy, lệnh không thực hiện được và trả về lỗi. IPC_NOWAIT: Cho phép tránh tình trạng chờ hoạt động. Nếu dãy trống, sẽ có lỗi trả về. Nếu không có tùy chọn này, hàm sẽ chờ đợi cho đến khi có thông báo phù hợp yêu cầu. Các lỗi có thể xảy ra khi nhận thông báo từ dãy: Lỗi Ý nghĩa EINVAL Bộ nhận dạng dãy thông báo không đúng, hoặc msgsize âm hay lớn hơn kích thước dãy quy định. EFAULT Vùng bộ nhớ do msgp chỉ đến không truy cập được. EACCES Tiến trình không đủ quyền truy cập dãy thông báo. ENOMSG Tùy chọn IPC_NOWAIT đã xác lập và không tìm thấy thông báo trong dãy. EINTR Tiến trình nhận một tín hiệu ngắt khi đang chờ một thông báo. Giao tiếp mạng Khái niệm socket và port Các socket được dùng trong lập trình mạng để truy cập và truyền thông tin. Có thể hiểu một cách khái quát về socket như cơ cấu truy cập file trên Unix được cung cấp cho một điểm cuối của kết nối. Tương tự như việc truy cập file, một ứng dụng có thể yêu cầu hệ thống cung cấp một socket khi nó cần. Hệ điều hành sẽ trả về một số nguyên mà ứng dụng có thể thông qua đó truy cập đến socket mới được tạo ra theo yêu cầu. Hầu hết các ứng dụng trên mạng sử dụng hai giao thức chính là: TCP (Transmission Control Protocol) hoặc UDP (User Datagram Protocol). Các số hiệu cổng dành cho các ứng dụng mạng sẽ để dàng cho hệ điều hành biết được các ứng dụng sử dụng các tài nguyên hệ thống và các dịch vụ được phép như thế nào. Socket là tổ hợp của hai thành phần là địa chỉ IP của máy tính và số hiệu cổng được sử dụng bởi phần mềm dùng giao thức TCP. Bởi vì phải có ít nhất hai máy kết nối truyền thông trên mạng, nên phải có một socket cho cả máy nhận và máy gởi thông tin. Địa chỉ IP của mỗi máy là đơn nhất và số hiệu cổng là một số đơn nhất trên mỗi máy nên socket cũng là đơn nhất trên mạng. Điều này cho phép một ứng dụng trao đổi thông tin với một ứng dụng khác quan hệ thống mạng một cách trọn vẹn thông qua socket. Linux hỗ trợ cả hai cách lập trình với socket là định hướng kết nối (connection oriented) và liên lạc không kết nối (connectionless communication). Đối với liên lạc định hướng kết nối, cả server và client phải thiết lập kết nối trước khi truyền dữ liệu. Đối với liên lạc không kết nối, thì server và client không cần thiết lập kết nối trước do đó dữ liệu được truyền như một thành phần của các thông báo truyền qua mạng. Tuy nhiên trong cả hai cách trên thì server luôn được khởi động trước, ràng buộc nó vào một socket và chờ đợi một kết nối đến từ client. Tạo ra một socket Để tạo ra một socket, chúng ta sử dụng hàm socket() được định nghĩa như sau: #include #include int socket(int family, int type, int protocol); Đối với họ Unix, family có giá trị là PF_UNIX, với họ TCP/IP internet là PF_INET, Xerox Corporation PUP internet là PF_PUP hay với mạng Apple Computer Incorporated Appletalk là PF_APPLETALK. Tham số type có một trong hai giá trị là SOCK_STREAM dành cho một kết nối có tin cậy cao nhưng ttốc độ truyền thông thấp, hoặc là SOCK_DGRAM có tốc độ truyền thông cao hơn nhưng độ tin cậy của kết nối thấp hơn. Riêng tham số protocol thường đưọc quy định là IPPROTO_TCP đối với SOCK_STREAM và IPPROTO_UDP cho giá trị SOCK_DGRAM của tham số thứ hai. Hàm trả về giá trị -1 nếu không thể tạo ra một socket. Nếu thành công, giá trị sẽ là bộ mô tả socket. Chúng ta sẽ sử dụng bộ mô tả socket để tham chiếu đến socket vừa tạo ra cho tất cả các lệnh có sử dụng socket tiếp theo trong chương trình. Khi được khởi tạo, một socket chưa được liên kết với một địa chỉ cục bộ hay địa chỉ đích nào cả. Đối với TCP/IP, điều này có nghĩa là chưa một cổng giao thức cục bộ nào được gán và không có một cổng đích hay địa chỉ IP cụ thể. Một khi có socket được tạo ra, chương trình server sẽ dùng hàm bind để đặt cho nó một đị chỉ cục bộ. Hàm bind() được định nghĩa như sau: #include #include int bind(int sockfd, struct sockaddr *saddr, int addrlen) Tham số đầu tiên là bộ mô tả socket, tiếp theo là một cấu trúc chứa địa chỉ cục bộ và tham số cuối là độ lớn của cấu trúc trước đó. Cấu trúc sockaddr gồm các thành phần: typedef unsigned char u_char; struct sockaddr{ u_char sa_len; /* Tổng độ dài của địa chỉ */ u_char sa_family; /* dòng (family) của địa chỉ */ char sa_data[14]; /* nội dung địa chỉ */ }; Trường sa_len chỉ độ dài của địa chỉ, trường thứ hai sa_family chỉ ra địa chỉ thuộc về một họ giao thức cụ thể (ví dụ như hằng PF_INET đại diện cho TCP/IP). Thâm số cuối cùng chứa các byte nội dung của địa chỉ. Mỗi họ giao thức đều có định nghĩa một cách chính xác định dạng của địa chỉ sử dụng trong trường sa_data của cấu trúc sockaddr. Ví dụ giao thức TCP/IP sử dụng cấu trúc sockaddr_in để định nghĩa địa chỉ như sau: struct sockaddr_in { u_char sin_len; u_char sin_family; u_short sin_port; /* Cổng số của giao thức */ struct in_addr sin_addr; /* Địa chỉ IP của máy*/ char sin_zero[8]; /* không sử dụng */ }; Trong cấu trúc trên, hai trường đầu tiên hoàn toàn giống cấu trúc sockaddr, trường thứ ba chứa cổng giao thức dành cho ứng dụng, trường sin_addr chứa địa chỉ IP của máy cục bộ. Cấu trúc này có trường sin_zero[8] không dùng đến vì hộ giao thức TCP/IP chỉ cần sáu byte để chứa địa chỉ máy và cổng cho ứng dụng. Như vậy để cấu trúc này đồng nhất kích thước với cấu trúc sockaddr, cần có thêm tám byte không sử dụng. Chờ đợi một kết nối từ client(server) Sau khi cụ thể một cổng cho socket, chương trình server phải cung cấp cho hệ điều hành chuyển socket vào chế độ thụ động để có thể chờ đợi một tiếp xúc từ một chương trình client. Chúng ta dùng hàm listen để thực hiện bước này. Hàm listen chỉ được gọi bởi chương trình server. #include #include int listen(int sockfd, int backlog); sockfd là bộ mô tả socket, backlog là một số biểu thị thời gian chờ đợi một kết nối trước khi loại bỏ. Hàm trả về giá trị nhỏ hơn 1 nếu gặp lỗi. Nếu có một kết nối đến trong thời gian chờ đợi, lúc này chúng ta sẽ chuyển sang bước kế tiếp để thực hiện một kết nối đến client. Hàm accept được server sử dụng để nhận lấy bất kỳ thông báo nào gởi đến từ hàm connect() từ chương trình client. #include #include int accept(int sockfd, struct sockaddr *peeraddr, int addrlen) Các tham số tương tự như hàm bind() đã xét, tuy nhiên peeraddr là một con trỏ chứa thông tin về máy khách đã có yêu cầu kết nối. Thực hiện kết nối đến server (client) Để có được một kết nối đến một server đang ở chế độ chờ kết nối, thì trước hết chương trình client phảo tạo ra một sock có các thành phần tham số giống như socket mà server đã ràng buộc vào đó. Sau đó thực hiện hàm connect() một khi chắc chắn rằng server đang chờ đợi kết nối đến. #include #include int connect(int sockfd, struct sockaddr *servsaddr, int addrlen) Các tham số tương tự trong hàm bind(). Ngoại trừ con trỏ servsaddr với cấu trúc struct sockaddr (thực chất chúng ta dùng struct sockaddr_in ) chứa các thông tin về chương trình server tại máy chủ như địa chỉ IP, số cổng của giao thức, ... Có thể nói ngắn gọn rằng hàm connect() là một công cụ để chương trình client tiếp xúc với chuơng trình server, gởi các thông tin của client đến máy chủ. Và chương trình server trả lời hành động tiếp xúc này bằng hàm accept() và sau đó kết nối được thiết lập. Sau khi kết nối được thiết lập, server và client có thể thực hiện việc trao đổi thông tin cho nhau cho đến khi một trong hai phía đóng kết nối bằng hàm: close(int socket); hoặc một trong hai bên bị ngắt đột ngột bởi hệ thống. Ví dụ mẫu mô hình client/server Mã lệnh chương trình server Trong ví dụ dưới đây chương trình server thực hiện các bước thiết lập cho việc chờ đợi một tiếp xúc từ chương trình client. Sau khi thiết lập kết nối với client, cả hai thực hiện một số thao tác truyền và nhận thông tin rồi kết thúc chương trình. Tạo ra một socket với hàm socket(). Ràng buộc sockket với một địa chỉ bằng hàm bind(). Dùng hàm listen() để chờ đợi một kết nối. Nhận bất kỳ thông tin nào yêu cầu kết nối bằng hàm accept(). Nhận các thông báo gởi đến bằng hàm read() và gởi thông báo dến client bằng hàm write(). #include #include #include #include #define MY_PORT 6545 main(int argc, char *argv[ ]) { int sockfd, newfd; int cpid; struct sockaddr_in servaddr; struct sockaddr_in clientInfo; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0) < 0) { myabort("Unable to create socket"); } bzero((char *)&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_family = htons(MY_PORT); if (bind(sockfd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr))< 0) { myabort("Unable to bind socket"); } listen(sockfd,5); for (;;) { newfd=accept(sockfd,(struct sockaddr *)&clientInfo, sizeof(struct sockaddr); if (newfd < 0) { myabort("Unable to accept on socket"); } if ((cpid = fork()) < 0) { myabort("Unable to fork on accept"); } else if (cpid = = 0) { /* child */ close(sockfd); /* no need for original */ do_your_thing(newfd); exit(0); } close(newfd); /* in the parent */ } } Mã lệnh chương trình client Từ chương trình client, để thực hiện được một kết nối đến server và truyền nhận thông tin chỉ cần thực hiện hai bước cơ bản như sau: Tạo một socket tương ứng với chương trình server cụ thể. Yêu cầu đến server thực hiện kết nối bằng cách gọi hàm connect(). Nếu một kết nối được tạo ra, client có thể gởi các yêu cầu bằng hàng write() và nhận các hồi âm trở lại với hàm read(). Trong cả hai chương trình này các hàm truyền nhận thông tin chưa được thiết kế các lệnh thành phần một cách cụ thể, phần này dành cho các bạn lập trình cần tìm hiểu đến. #include #include #include #include #define MY_PORT 6545 #define MY_HOST_ADDR "204.25.13.1" int getServerSocketId() { int fd, len; struct sockaddr_in unix_addr; /* create a Unix domain stream socket */ if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { return(-1); } /* fill socket address structure w/our address */ memset(&unix_addr, 0, sizeof(unix_addr)); unix_addr.sin_family = AF_INET; /* convert internet address to binary value*/ unix_addr.sin_addr.s_addr = inet_addr(MY_HOST_ADDR); unix_addr.sin_family = htons(MY_PORT); if (bind(fd, (struct sockaddr *) &unix_addr, len) < 0) return(-2); memset(&unix_addr, 0, sizeof(unix_addr)); if (connect(fd, (struct sockaddr *) &unix_addr, len) < 0) return(-3); return(fd); } Ngôn ngữ tcl/tk Khái niệm Khái niệm tcl (tool command language) là một ngôn ngữ thông dịch đơn giản của lớp võ (shell) Linux. tk (toolkit) là bộ công cụ sử dụng cú pháp tcl tạo ra các giao diện người sử dụng bằng đồ họa (GUI) hợp thành từ các thành phần như các nút bấm, thanh trượt, hộp thoại, các cửa sổ, ... Khi thực hiện một chương trình tcl thì tcl shell (tclsh) hoặc windowing shell (wish) sẽ được gọi. Cả tclsh và wish đều là những shell chuẫn của Linux, cả hai đều cho phép thực hiện lệnh ở chế độ tương tác hoặc được đọc ra từ một tập tin. Trong thực tế, chế độ tương tác trực tiếp hiếm khi được sử dụng và khả năng của chế độ này rất hạn chế. Tuy cả tclsh và wish đều có thể thực hiện một chương trình tcl, những giữa chúng có điểm khác nhau cơ bản là tclsh chỉ có thể hiểu các lệnh tcl, trong khi wish có thể hiểu cả các lệnh tcl và tk. Các thành phần cơ bản của tcl/tk Cấu trúc lệnh Cấu trúc cơ bản của một lệnh tcl là: tênlệnh tham_số Tên lệnh là ký hiệu lệnh của tcl để thực thi, tham_số là những đối số lựa chọn của câu lệnh đó. Cả hai thành phần trên được gọi là lệnh. Một lênh được kết thực bằng một ký tự xuống hàng (\n) hay một dấu chấm phẩy (;). Nếu trên một dòng chỉ có một lenh thì không cần phải có dấu chấm phẩy. set foo 0; set bar 1 Trên đây là hai lệnh minh họa ch

Các file đính kèm theo tài liệu này:

  • docLYTHUYET.DOC
  • rarCHUONGTRINH.rar
Tài liệu liên quan